Add Attach Database dialog (#24225)

This commit is contained in:
Cory Rivera
2023-08-30 13:38:25 -07:00
committed by GitHub
parent 9eee1384b4
commit e4abe4d167
9 changed files with 342 additions and 3 deletions

View File

@@ -117,6 +117,11 @@
"category": "MSSQL", "category": "MSSQL",
"title": "%title.detachDatabase%" "title": "%title.detachDatabase%"
}, },
{
"command": "mssql.attachDatabase",
"category": "MSSQL",
"title": "%title.attachDatabase%"
},
{ {
"command": "mssql.enableGroupBySchema", "command": "mssql.enableGroupBySchema",
"category": "MSSQL", "category": "MSSQL",
@@ -525,6 +530,10 @@
{ {
"command": "mssql.detachDatabase", "command": "mssql.detachDatabase",
"when": "false" "when": "false"
},
{
"command": "mssql.attachDatabase",
"when": "false"
} }
], ],
"objectExplorer/item/context": [ "objectExplorer/item/context": [
@@ -589,6 +598,11 @@
"when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && !(nodePath =~ /^.*\\/System Databases\\/.*$/) && config.workbench.enablePreviewFeatures", "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && !(nodePath =~ /^.*\\/System Databases\\/.*$/) && config.workbench.enablePreviewFeatures",
"group": "1_objectManagement@3" "group": "1_objectManagement@3"
}, },
{
"command": "mssql.attachDatabase",
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Databases && !isCloud && config.workbench.enablePreviewFeatures",
"group": "1_objectManagement"
},
{ {
"command": "mssql.enableGroupBySchema", "command": "mssql.enableGroupBySchema",
"when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema", "when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema",

View File

@@ -190,5 +190,6 @@
"title.objectProperties": "Properties (Preview)", "title.objectProperties": "Properties (Preview)",
"title.dropObject": "Drop (Preview)", "title.dropObject": "Drop (Preview)",
"title.renameObject": "Rename (Preview)", "title.renameObject": "Rename (Preview)",
"title.detachDatabase": "Detach (Preview)" "title.detachDatabase": "Detach (Preview)",
"title.attachDatabase": "Attach (Preview)"
} }

View File

@@ -8,6 +8,7 @@ import * as telemetry from '@microsoft/ads-extension-telemetry';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { ConnectParams } from 'dataprotocol-client/lib/protocol'; import { ConnectParams } from 'dataprotocol-client/lib/protocol';
import * as mssql from 'mssql'; import * as mssql from 'mssql';
import { DatabaseFileData } from 'mssql';
// ------------------------------- < Telemetry Sent Event > ------------------------------------ // ------------------------------- < Telemetry Sent Event > ------------------------------------
@@ -1673,6 +1674,33 @@ export namespace DropDatabaseRequest {
export const type = new RequestType<DropDatabaseRequestParams, string, void, void>('objectManagement/dropDatabase'); export const type = new RequestType<DropDatabaseRequestParams, string, void, void>('objectManagement/dropDatabase');
} }
export interface AttachDatabaseRequestParams {
connectionUri: string;
databases: DatabaseFileData[];
generateScript: boolean;
}
export namespace AttachDatabaseRequest {
export const type = new RequestType<AttachDatabaseRequestParams, string, void, void>('objectManagement/attachDatabase');
}
export interface GetDataFolderRequestParams {
connectionUri: string;
}
export namespace GetDataFolderRequest {
export const type = new RequestType<GetDataFolderRequestParams, string, void, void>('admin/getdatafolder');
}
export interface GetAssociatedFilesRequestParams {
connectionUri: string;
primaryFilePath: string;
}
export namespace GetAssociatedFilesRequest {
export const type = new RequestType<GetAssociatedFilesRequestParams, string[], void, void>('admin/getassociatedfiles');
}
// ------------------------------- < Object Management > ------------------------------------ // ------------------------------- < Object Management > ------------------------------------
// ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------ // ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------

View File

@@ -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. * @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<string>; detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Thenable<string>;
/**
* 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<string>;
/** /**
* Drop a database. * Drop a database.
* @param connectionUri The URI of the server connection. * @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. * @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<string>; dropDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, deleteBackupHistory: boolean, generateScript: boolean): Thenable<string>;
/**
* 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<string>;
/**
* 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<string[]>;
}
export interface DatabaseFileData {
databaseName: string;
databaseFilePaths: string[];
owner: string;
} }
// Object Management - End. // Object Management - End.
} }

View File

@@ -24,7 +24,8 @@ import { ApplicationRoleDialog } from './ui/applicationRoleDialog';
import { DatabaseDialog } from './ui/databaseDialog'; import { DatabaseDialog } from './ui/databaseDialog';
import { ServerPropertiesDialog } from './ui/serverPropertiesDialog'; import { ServerPropertiesDialog } from './ui/serverPropertiesDialog';
import { DetachDatabaseDialog } from './ui/detachDatabaseDialog'; 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) { export function registerObjectManagementCommands(appContext: AppContext) {
// Notes: Change the second parameter to false to use the actual object management service. // 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) => { appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.detachDatabase', async (context: azdata.ObjectExplorerContext) => {
await handleDetachDatabase(context, service); 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) => { appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.dropDatabase', async (context: azdata.ObjectExplorerContext) => {
await handleDropDatabase(context, service); await handleDropDatabase(context, service);
})); }));
@@ -290,6 +294,34 @@ async function handleDetachDatabase(context: azdata.ObjectExplorerContext, servi
} }
} }
async function handleAttachDatabase(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
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<void> { async function handleDropDatabase(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
const connectionUri = await getConnectionUri(context); const connectionUri = await getConnectionUri(context);
if (!connectionUri) { if (!connectionUri) {

View File

@@ -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 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 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 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 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 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'; 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', RenameObject = 'RenameObject',
UpdateObject = 'UpdateObject', UpdateObject = 'UpdateObject',
OpenDetachDatabaseDialog = 'OpenDetachDatabaseDialog', OpenDetachDatabaseDialog = 'OpenDetachDatabaseDialog',
OpenAttachDatabaseDialog = 'OpenAttachDatabaseDialog',
OpenDropDatabaseDialog = 'OpenDropDatabaseDialog' OpenDropDatabaseDialog = 'OpenDropDatabaseDialog'
} }

View File

@@ -47,8 +47,12 @@ export const GrantColumnHeader = localize('objectManagement.grantColumnHeader',
export const WithGrantColumnHeader = localize('objectManagement.withGrantColumnHeader', "With Grant"); export const WithGrantColumnHeader = localize('objectManagement.withGrantColumnHeader', "With Grant");
export const DenyColumnHeader = localize('objectManagement.denyColumnHeader', "Deny"); export const DenyColumnHeader = localize('objectManagement.denyColumnHeader', "Deny");
export const SelectSecurablesDialogTitle = localize('objectManagement.selectSecurablesDialogTitle', "Select Securables"); 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 CreateObjectLabel = localize('objectManagement.createObjectLabel', "Create");
export const ApplyUpdatesLabel = localize('objectManagement.applyUpdatesLabel', "Apply"); 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 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); } 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); }, "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 { export function OpenObjectPropertiesDialogError(objectType: string, objectName: string, error: string): string {
return localize({ return localize({
key: 'objectManagement.openObjectPropertiesDialogError', key: 'objectManagement.openObjectPropertiesDialogError',
@@ -190,6 +201,15 @@ export const DatabaseFilePathLabel = localize('objectManagement.databaseFilePath
export const DatabaseFileGroupLabel = localize('objectManagement.databaseFileGroup', "File Group"); export const DatabaseFileGroupLabel = localize('objectManagement.databaseFileGroup', "File Group");
export const DetachDatabaseOptions = localize('objectManagement.detachDatabaseOptions', "Detach Database Options"); export const DetachDatabaseOptions = localize('objectManagement.detachDatabaseOptions', "Detach Database Options");
export const DetachButtonLabel = localize('objectManagement.detachButtonLabel', "Detach"); 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 DropDatabaseDialogTitle = (dbName: string) => localize('objectManagement.dropDatabaseDialogTitle', "Drop Database - {0} (Preview)", dbName);
export const DropButtonLabel = localize('objectManagement.dropButtonLabel', "Drop"); export const DropButtonLabel = localize('objectManagement.dropButtonLabel', "Drop");
export const DropDatabaseOptions = localize('objectManagement.dropDatabaseOptions', "Drop Database Options"); export const DropDatabaseOptions = localize('objectManagement.dropDatabaseOptions', "Drop Database Options");

View File

@@ -8,7 +8,7 @@ import * as constants from '../constants';
import * as contracts from '../contracts'; import * as contracts from '../contracts';
import { BaseService, ISqlOpsFeature, SqlOpsDataClient } from 'dataprotocol-client'; import { BaseService, ISqlOpsFeature, SqlOpsDataClient } from 'dataprotocol-client';
import { ObjectManagement, IObjectManagementService } from 'mssql'; import { ObjectManagement, IObjectManagementService, DatabaseFileData } from 'mssql';
import { ClientCapabilities } from 'vscode-languageclient'; import { ClientCapabilities } from 'vscode-languageclient';
import { AppContext } from '../appContext'; import { AppContext } from '../appContext';
@@ -75,6 +75,21 @@ export class ObjectManagementService extends BaseService implements IObjectManag
const params: contracts.DropDatabaseRequestParams = { connectionUri, objectUrn, dropConnections, deleteBackupHistory, generateScript }; const params: contracts.DropDatabaseRequestParams = { connectionUri, objectUrn, dropConnections, deleteBackupHistory, generateScript };
return this.runWithErrorHandling(contracts.DropDatabaseRequest.type, params); return this.runWithErrorHandling(contracts.DropDatabaseRequest.type, params);
} }
async attachDatabases(connectionUri: string, databases: DatabaseFileData[], generateScript: boolean): Promise<string> {
const params: contracts.AttachDatabaseRequestParams = { connectionUri, databases, generateScript };
return this.runWithErrorHandling(contracts.AttachDatabaseRequest.type, params);
}
async getDataFolder(connectionUri: string): Promise<string> {
const params: contracts.GetDataFolderRequestParams = { connectionUri };
return this.runWithErrorHandling(contracts.GetDataFolderRequest.type, params);
}
async getAssociatedFiles(connectionUri: string, primaryFilePath: string): Promise<string[]> {
const params: contracts.GetAssociatedFilesRequestParams = { connectionUri, primaryFilePath };
return this.runWithErrorHandling(contracts.GetAssociatedFilesRequest.type, params);
}
} }
const ServerLevelSecurableTypes: SecurableTypeMetadata[] = [ const ServerLevelSecurableTypes: SecurableTypeMetadata[] = [
@@ -246,10 +261,22 @@ export class TestObjectManagementService implements IObjectManagementService {
return this.delayAndResolve(''); return this.delayAndResolve('');
} }
async attachDatabases(connectionUri: string, databases: DatabaseFileData[], generateScript: boolean): Promise<string> {
return this.delayAndResolve('');
}
dropDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, deleteBackupHistory: boolean, generateScript: boolean): Thenable<string> { dropDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, deleteBackupHistory: boolean, generateScript: boolean): Thenable<string> {
return this.delayAndResolve(''); return this.delayAndResolve('');
} }
async getDataFolder(connectionUri: string): Promise<string> {
return this.delayAndResolve('');
}
async getAssociatedFiles(connectionUri: string, primaryFilePath: string): Promise<string[]> {
return this.delayAndResolve([]);
}
private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] { private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] {
let items: ObjectManagement.SearchResultItem[] = []; let items: ObjectManagement.SearchResultItem[] = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {

View File

@@ -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<Database, DatabaseViewInfo> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string[]> {
let errors = [];
if (this._databasesToAttach.length === 0) {
errors.push(loc.NoDatabaseFilesError);
}
return errors;
}
protected override async saveChanges(contextId: string, object: ObjectManagement.SqlObject): Promise<void> {
await this.objectManagementService.attachDatabases(this.options.connectionUri, this._databasesToAttach, false);
}
protected override async generateScript(): Promise<string> {
return await this.objectManagementService.attachDatabases(this.options.connectionUri, this._databasesToAttach, true);
}
}