From e4abe4d167c5fe0bb0ef55ab918456e66f90ca8e Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Wed, 30 Aug 2023 13:38:25 -0700 Subject: [PATCH] Add Attach Database dialog (#24225) --- extensions/mssql/package.json | 14 ++ extensions/mssql/package.nls.json | 3 +- extensions/mssql/src/contracts.ts | 28 +++ extensions/mssql/src/mssql.d.ts | 27 +++ .../mssql/src/objectManagement/commands.ts | 34 +++- .../mssql/src/objectManagement/constants.ts | 2 + .../objectManagement/localizedConstants.ts | 20 ++ .../objectManagementService.ts | 29 ++- .../ui/attachDatabaseDialog.ts | 188 ++++++++++++++++++ 9 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 extensions/mssql/src/objectManagement/ui/attachDatabaseDialog.ts diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 9b6b89fa67..ef21e5a922 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -117,6 +117,11 @@ "category": "MSSQL", "title": "%title.detachDatabase%" }, + { + "command": "mssql.attachDatabase", + "category": "MSSQL", + "title": "%title.attachDatabase%" + }, { "command": "mssql.enableGroupBySchema", "category": "MSSQL", @@ -525,6 +530,10 @@ { "command": "mssql.detachDatabase", "when": "false" + }, + { + "command": "mssql.attachDatabase", + "when": "false" } ], "objectExplorer/item/context": [ @@ -589,6 +598,11 @@ "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && !(nodePath =~ /^.*\\/System Databases\\/.*$/) && config.workbench.enablePreviewFeatures", "group": "1_objectManagement@3" }, + { + "command": "mssql.attachDatabase", + "when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Databases && !isCloud && config.workbench.enablePreviewFeatures", + "group": "1_objectManagement" + }, { "command": "mssql.enableGroupBySchema", "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema", diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index cee902fae7..01c00ab891 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -190,5 +190,6 @@ "title.objectProperties": "Properties (Preview)", "title.dropObject": "Drop (Preview)", "title.renameObject": "Rename (Preview)", - "title.detachDatabase": "Detach (Preview)" + "title.detachDatabase": "Detach (Preview)", + "title.attachDatabase": "Attach (Preview)" } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 99cc33d9c1..f8c9fc4bb2 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -8,6 +8,7 @@ import * as telemetry from '@microsoft/ads-extension-telemetry'; import * as azdata from 'azdata'; import { ConnectParams } from 'dataprotocol-client/lib/protocol'; import * as mssql from 'mssql'; +import { DatabaseFileData } from 'mssql'; // ------------------------------- < Telemetry Sent Event > ------------------------------------ @@ -1673,6 +1674,33 @@ export namespace DropDatabaseRequest { export const type = new RequestType('objectManagement/dropDatabase'); } +export interface AttachDatabaseRequestParams { + connectionUri: string; + databases: DatabaseFileData[]; + generateScript: boolean; +} + +export namespace AttachDatabaseRequest { + export const type = new RequestType('objectManagement/attachDatabase'); +} + +export interface GetDataFolderRequestParams { + connectionUri: string; +} + +export namespace GetDataFolderRequest { + export const type = new RequestType('admin/getdatafolder'); +} + +export interface GetAssociatedFilesRequestParams { + connectionUri: string; + primaryFilePath: string; +} + +export namespace GetAssociatedFilesRequest { + export const type = new RequestType('admin/getassociatedfiles'); +} + // ------------------------------- < Object Management > ------------------------------------ // ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------ diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index e75d4431ba..f8c46af4db 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -984,6 +984,14 @@ declare module 'mssql' { * @returns A string value representing the generated TSQL query if generateScript was set to true, and an empty string otherwise. */ detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Thenable; + /** + * Attach one or more databases. + * @param connectionUri The URI of the server connection. + * @param databases The name, owner, and file paths for each database that will be attached. + * @param generateScript Whether to generate a TSQL script for the operation instead of detaching the database. + * @returns A string value representing the generated TSQL query if generateScript was set to true, and an empty string otherwise. + */ + attachDatabases(connectionUri: string, databases: DatabaseFileData[], generateScript: boolean): Thenable; /** * Drop a database. * @param connectionUri The URI of the server connection. @@ -994,6 +1002,25 @@ declare module 'mssql' { * @returns A string value representing the generated TSQL query if generateScript was set to true, and an empty string otherwise. */ dropDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, deleteBackupHistory: boolean, generateScript: boolean): Thenable; + /** + * Gets the file path for the default database file folder for a SQL Server instance. + * @param connectionUri The URI of the connection for the specific server. + * @returns The file path to the data folder. + */ + getDataFolder(connectionUri: string): Thenable; + /** + * Retrieves other database files associated with a specified primary file, such as Data, Log, and FileStream files. + * @param connectionUri The URI of the connection for the specific server. + * @param primaryFilePath The file path for the primary database file on the target server. + * @returns An array of file path strings for each of the associated files. + */ + getAssociatedFiles(connectionUri: string, primaryFilePath: string): Thenable; + } + + export interface DatabaseFileData { + databaseName: string; + databaseFilePaths: string[]; + owner: string; } // Object Management - End. } diff --git a/extensions/mssql/src/objectManagement/commands.ts b/extensions/mssql/src/objectManagement/commands.ts index add4e48fc1..5056add9c6 100644 --- a/extensions/mssql/src/objectManagement/commands.ts +++ b/extensions/mssql/src/objectManagement/commands.ts @@ -24,7 +24,8 @@ import { ApplicationRoleDialog } from './ui/applicationRoleDialog'; import { DatabaseDialog } from './ui/databaseDialog'; import { ServerPropertiesDialog } from './ui/serverPropertiesDialog'; import { DetachDatabaseDialog } from './ui/detachDatabaseDialog'; -import { DropDatabaseDialog as DropDatabaseDialog } from './ui/dropDatabaseDialog'; +import { DropDatabaseDialog } from './ui/dropDatabaseDialog'; +import { AttachDatabaseDialog } from './ui/attachDatabaseDialog'; export function registerObjectManagementCommands(appContext: AppContext) { // Notes: Change the second parameter to false to use the actual object management service. @@ -47,6 +48,9 @@ export function registerObjectManagementCommands(appContext: AppContext) { appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.detachDatabase', async (context: azdata.ObjectExplorerContext) => { await handleDetachDatabase(context, service); })); + appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.attachDatabase', async (context: azdata.ObjectExplorerContext) => { + await handleAttachDatabase(context, service); + })); appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.dropDatabase', async (context: azdata.ObjectExplorerContext) => { await handleDropDatabase(context, service); })); @@ -290,6 +294,34 @@ async function handleDetachDatabase(context: azdata.ObjectExplorerContext, servi } } +async function handleAttachDatabase(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise { + const connectionUri = await getConnectionUri(context); + if (!connectionUri) { + return; + } + try { + const parentUrn = await getParentUrn(context); + const options: ObjectManagementDialogOptions = { + connectionUri: connectionUri, + isNewObject: true, + database: context.connectionProfile!.databaseName!, + objectType: ObjectManagement.NodeType.Database, + objectName: '', + parentUrn: parentUrn, + objectExplorerContext: context + }; + const dialog = new AttachDatabaseDialog(service, options); + await dialog.open(); + } + catch (err) { + TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.OpenAttachDatabaseDialog, err).withAdditionalProperties({ + objectType: context.nodeInfo!.nodeType + }).send(); + console.error(err); + await vscode.window.showErrorMessage(objectManagementLoc.OpenAttachDatabaseDialogError(getErrorMessage(err))); + } +} + async function handleDropDatabase(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise { const connectionUri = await getConnectionUri(context); if (!connectionUri) { diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index 0cc9cfd267..8536ed4fc5 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -33,6 +33,7 @@ export const ViewMemoryServerPropertiesDocUrl = 'https://learn.microsoft.com/sql export const ViewProcessorsServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/database-engine/configure-windows/server-properties-processors-page'; export const ViewSecurityServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/database-engine/configure-windows/server-properties-security-page'; export const DetachDatabaseDocUrl = 'https://go.microsoft.com/fwlink/?linkid=2240322'; +export const AttachDatabaseDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/attach-a-database#to-attach-a-database'; export const DatabaseGeneralPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-general-page'; export const DatabaseOptionsPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-options-page' export const DropDatabaseDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/drop-database-transact-sql'; @@ -47,6 +48,7 @@ export const enum TelemetryActions { RenameObject = 'RenameObject', UpdateObject = 'UpdateObject', OpenDetachDatabaseDialog = 'OpenDetachDatabaseDialog', + OpenAttachDatabaseDialog = 'OpenAttachDatabaseDialog', OpenDropDatabaseDialog = 'OpenDropDatabaseDialog' } diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index b8f5a21219..0391a5bba7 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -47,8 +47,12 @@ export const GrantColumnHeader = localize('objectManagement.grantColumnHeader', export const WithGrantColumnHeader = localize('objectManagement.withGrantColumnHeader', "With Grant"); export const DenyColumnHeader = localize('objectManagement.denyColumnHeader', "Deny"); export const SelectSecurablesDialogTitle = localize('objectManagement.selectSecurablesDialogTitle', "Select Securables"); +export const AddFileAriaLabel = localize('objectManagement.addFileText', "Add database files"); +export const RemoveFileAriaLabel = localize('objectManagement.removeFileText', "Remove database file"); export const CreateObjectLabel = localize('objectManagement.createObjectLabel', "Create"); export const ApplyUpdatesLabel = localize('objectManagement.applyUpdatesLabel', "Apply"); +export const DataFileLabel = localize('objectManagement.dataFileLabel', "Data"); +export const LogFileLabel = localize('objectManagement.logFileLabel', "Log"); export function ExplicitPermissionsTableLabelSelected(name: string): string { return localize('objectManagement.explicitPermissionsTableLabelSelected', "Explicit permissions for: {0}", name); } export function EffectivePermissionsTableLabelSelected(name: string): string { return localize('objectManagement.effectivePermissionsTableLabelSelected', "Effective permissions for: {0}", name); } @@ -109,6 +113,13 @@ export function OpenDropDatabaseDialogError(error: string): string { }, "An error occurred while opening the drop database dialog. {0}", error); } +export function OpenAttachDatabaseDialogError(error: string): string { + return localize({ + key: 'objectManagement.openAttachDatabaseDialogError', + comment: ['{0}: error message.'] + }, "An error occurred while opening the attach database dialog. {0}", error); +} + export function OpenObjectPropertiesDialogError(objectType: string, objectName: string, error: string): string { return localize({ key: 'objectManagement.openObjectPropertiesDialogError', @@ -190,6 +201,15 @@ export const DatabaseFilePathLabel = localize('objectManagement.databaseFilePath export const DatabaseFileGroupLabel = localize('objectManagement.databaseFileGroup', "File Group"); export const DetachDatabaseOptions = localize('objectManagement.detachDatabaseOptions', "Detach Database Options"); export const DetachButtonLabel = localize('objectManagement.detachButtonLabel', "Detach"); +export const AttachDatabaseDialogTitle = localize('objectManagement.attachDatabaseDialogTitle', "Attach Database (Preview)"); +export const NoDatabaseFilesError = localize('objectManagement.doDatabaseFilesError', "No database files were specified to attach to the server."); +export const DatabasesToAttachLabel = localize('objectManagement.databasesToAttach', "Databases to Attach"); +export const AssociatedFilesLabel = localize('objectManagement.associatedDatabaseFiles', "Associated Database Files"); +export const MdfFileLocation = localize('objectManagement.mdfFileLocation', "MDF File Location"); +export const DatabaseFilesFilterLabel = localize('objectManagement.databaseFilesFilterLabel', "Database Data Files") +export const DatabaseName = localize('objectManagement.databaseName', "DB Name"); +export const AttachAsText = localize('objectManagement.attachAsText', "Attach As"); +export const AttachButtonLabel = localize('objectManagement.attachButtonLabel', "Attach"); export const DropDatabaseDialogTitle = (dbName: string) => localize('objectManagement.dropDatabaseDialogTitle', "Drop Database - {0} (Preview)", dbName); export const DropButtonLabel = localize('objectManagement.dropButtonLabel', "Drop"); export const DropDatabaseOptions = localize('objectManagement.dropDatabaseOptions', "Drop Database Options"); diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 468ef702b8..393f1c9012 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -8,7 +8,7 @@ import * as constants from '../constants'; import * as contracts from '../contracts'; import { BaseService, ISqlOpsFeature, SqlOpsDataClient } from 'dataprotocol-client'; -import { ObjectManagement, IObjectManagementService } from 'mssql'; +import { ObjectManagement, IObjectManagementService, DatabaseFileData } from 'mssql'; import { ClientCapabilities } from 'vscode-languageclient'; import { AppContext } from '../appContext'; @@ -75,6 +75,21 @@ export class ObjectManagementService extends BaseService implements IObjectManag const params: contracts.DropDatabaseRequestParams = { connectionUri, objectUrn, dropConnections, deleteBackupHistory, generateScript }; return this.runWithErrorHandling(contracts.DropDatabaseRequest.type, params); } + + async attachDatabases(connectionUri: string, databases: DatabaseFileData[], generateScript: boolean): Promise { + const params: contracts.AttachDatabaseRequestParams = { connectionUri, databases, generateScript }; + return this.runWithErrorHandling(contracts.AttachDatabaseRequest.type, params); + } + + async getDataFolder(connectionUri: string): Promise { + const params: contracts.GetDataFolderRequestParams = { connectionUri }; + return this.runWithErrorHandling(contracts.GetDataFolderRequest.type, params); + } + + async getAssociatedFiles(connectionUri: string, primaryFilePath: string): Promise { + const params: contracts.GetAssociatedFilesRequestParams = { connectionUri, primaryFilePath }; + return this.runWithErrorHandling(contracts.GetAssociatedFilesRequest.type, params); + } } const ServerLevelSecurableTypes: SecurableTypeMetadata[] = [ @@ -246,10 +261,22 @@ export class TestObjectManagementService implements IObjectManagementService { return this.delayAndResolve(''); } + async attachDatabases(connectionUri: string, databases: DatabaseFileData[], generateScript: boolean): Promise { + return this.delayAndResolve(''); + } + dropDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, deleteBackupHistory: boolean, generateScript: boolean): Thenable { return this.delayAndResolve(''); } + async getDataFolder(connectionUri: string): Promise { + return this.delayAndResolve(''); + } + + async getAssociatedFiles(connectionUri: string, primaryFilePath: string): Promise { + return this.delayAndResolve([]); + } + private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] { let items: ObjectManagement.SearchResultItem[] = []; for (let i = 0; i < count; i++) { diff --git a/extensions/mssql/src/objectManagement/ui/attachDatabaseDialog.ts b/extensions/mssql/src/objectManagement/ui/attachDatabaseDialog.ts new file mode 100644 index 0000000000..3d5368305e --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/attachDatabaseDialog.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; +import { DatabaseFileData, IObjectManagementService, ObjectManagement } from 'mssql'; +import { Database, DatabaseViewInfo } from '../interfaces'; +import { AttachDatabaseDocUrl } from '../constants'; +import * as loc from '../localizedConstants'; +import { RemoveText } from '../../ui/localizedConstants'; +import { DefaultMinTableRowCount, DialogButton, getTableHeight } from '../../ui/dialogBase'; +import path = require('path'); +import { getErrorMessage } from '../../utils'; + +export class AttachDatabaseDialog extends ObjectManagementDialogBase { + private _databasesToAttach: DatabaseFileData[] = []; + private _databasesTable: azdata.TableComponent; + private _associatedFilesTable: azdata.TableComponent; + private _databaseFiles: string[][] = []; + private readonly _fileFilters: azdata.window.FileFilters[] = [{ label: loc.DatabaseFilesFilterLabel, filters: ['*.mdf'] }]; + + private _nameField: azdata.InputBoxComponent; + private _nameContainer: azdata.FlexContainer; + + private _ownerDropdown: azdata.DropDownComponent; + private _ownerContainer: azdata.FlexContainer; + + constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { + super(objectManagementService, options, loc.AttachDatabaseDialogTitle, 'AttachDatabase'); + this.dialogObject.okButton.label = loc.AttachButtonLabel; + } + + protected override get isDirty(): boolean { + return this._databasesToAttach.length > 0; + } + + protected async initializeUI(): Promise { + let filesSection = this.initializeAttachSection(); + let associatedSection = this.initializeAssociatedFilesSection(); + this.formContainer.addItems([filesSection, associatedSection]); + } + + private initializeAttachSection(): azdata.GroupContainer { + const columns = [loc.MdfFileLocation, loc.DatabaseName]; + this._databasesTable = this.createTable(loc.DatabasesToAttachLabel, columns, []); + this.disposables.push(this._databasesTable.onRowSelected(() => this.onFileRowSelected())) + + let addButton: DialogButton = { + buttonAriaLabel: loc.AddFileAriaLabel, + buttonHandler: async () => await this.onAddFilesButtonClicked() + }; + let removeButton: DialogButton = { + buttonAriaLabel: RemoveText, + buttonHandler: async () => await this.onRemoveFilesButtonClicked() + }; + const buttonContainer = this.addButtonsForTable(this._databasesTable, addButton, removeButton); + + this._nameField = this.createInputBox(async newValue => { + let selectedRow = this._databasesTable.selectedRows[0]; + let dbFile = this._databasesToAttach[selectedRow]; + dbFile.databaseName = newValue; + }, {}); + this._nameContainer = this.createLabelInputContainer(loc.AttachAsText, this._nameField); + + this._ownerDropdown = this.createDropdown(loc.OwnerText, async newValue => { + let selectedRow = this._databasesTable.selectedRows[0]; + let dbFile = this._databasesToAttach[selectedRow]; + dbFile.owner = newValue; + }, this.viewInfo.loginNames.options, this.viewInfo.loginNames.options[this.viewInfo.loginNames.defaultValueIndex]); + this._ownerContainer = this.createLabelInputContainer(loc.OwnerText, this._ownerDropdown); + + // Hide input controls until we have files in the table + this._nameContainer.display = 'none'; + this._ownerContainer.display = 'none'; + + return this.createGroup(loc.DatabasesToAttachLabel, [this._databasesTable, buttonContainer, this._nameContainer, this._ownerContainer], false); + } + + private initializeAssociatedFilesSection(): azdata.GroupContainer { + const columns = [loc.DatabaseFileNameLabel, loc.DatabaseFileTypeLabel, loc.DatabaseFilePathLabel]; + this._associatedFilesTable = this.createTable(loc.DatabaseFilesLabel, columns, []); + return this.createGroup(loc.AssociatedFilesLabel, [this._associatedFilesTable], false); + } + + private async onFileRowSelected(): Promise { + if (this._databasesTable.selectedRows?.length > 0) { + let selectedRow = this._databasesTable.selectedRows[0]; + let dbFile = this._databasesToAttach[selectedRow]; + + this._nameField.value = dbFile.databaseName; + this._ownerDropdown.value = dbFile.owner; + + await this.updateAssociatedFilesTable(dbFile.databaseFilePaths); + } else { + await this.updateAssociatedFilesTable([]); + } + } + + private async updateAssociatedFilesTable(filePaths: string[]): Promise { + let tableRows = filePaths.map(filePath => { + let ext = path.extname(filePath); + let fileType = ext === '.ldf' ? loc.LogFileLabel : loc.DataFileLabel; + let fileName = path.basename(filePath, ext); + return [fileName, fileType, filePath]; + }); + await this._associatedFilesTable.updateProperties({ + data: tableRows, + height: getTableHeight(tableRows.length, DefaultMinTableRowCount) + }); + } + + private async onAddFilesButtonClicked(): Promise { + try { + let dataFolder = await this.objectManagementService.getDataFolder(this.options.connectionUri); + let filePath = await azdata.window.openServerFileBrowserDialog(this.options.connectionUri, dataFolder, this._fileFilters); + if (filePath) { + let owner = this.viewInfo.loginNames?.options[this.viewInfo.loginNames.defaultValueIndex]; + let fileName = path.basename(filePath, path.extname(filePath)); + let tableRow = [filePath, fileName]; + + // Associated files will also include the primary file, so we don't need to add it to the array again + let associatedFiles = await this.objectManagementService.getAssociatedFiles(this.options.connectionUri, filePath) ?? []; + + this._databaseFiles.push(tableRow); + this._databasesToAttach.push({ databaseName: fileName, databaseFilePaths: associatedFiles, owner }); + + this._nameContainer.display = 'block'; + this._ownerContainer.display = 'block'; + + await this.updateTableData(); + this._databasesTable.setActiveCell(this._databasesToAttach.length - 1, 0); + } + } catch (error) { + this.dialogObject.message = { + text: getErrorMessage(error), + level: azdata.window.MessageLevel.Error + }; + } + } + + private async onRemoveFilesButtonClicked(): Promise { + let selectedRows = this._databasesTable.selectedRows; + let deletedRowCount = 0; + for (let row of selectedRows) { + let index = row - deletedRowCount; + this._databaseFiles.splice(index, 1); + this._databasesToAttach.splice(index, 1); + deletedRowCount++; + } + if (this._databasesToAttach.length === 0) { + this._nameContainer.display = 'none'; + this._ownerContainer.display = 'none'; + } else { + this._databasesTable.setActiveCell(0, 0); + } + await this.updateTableData(); + } + + private async updateTableData(): Promise { + await this._databasesTable.updateProperties({ + data: this._databaseFiles, + height: getTableHeight(this._databaseFiles.length, DefaultMinTableRowCount) + }); + this.onFormFieldChange(); + } + + protected override get helpUrl(): string { + return AttachDatabaseDocUrl; + } + + protected override async validateInput(): Promise { + let errors = []; + if (this._databasesToAttach.length === 0) { + errors.push(loc.NoDatabaseFilesError); + } + return errors; + } + + protected override async saveChanges(contextId: string, object: ObjectManagement.SqlObject): Promise { + await this.objectManagementService.attachDatabases(this.options.connectionUri, this._databasesToAttach, false); + } + + protected override async generateScript(): Promise { + return await this.objectManagementService.attachDatabases(this.options.connectionUri, this._databasesToAttach, true); + } +}