From 1b8d66bba764fba1134dcb0adc49192b78c6b7e3 Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 20 Oct 2025 15:35:19 -0600 Subject: [PATCH 01/79] proof of concept --- package.json | 31 + package.nls.json | 5 + scripts/bundle-reactviews.js | 1 + src/constants/constants.ts | 5 + src/constants/locConstants.ts | 33 + .../dataTierApplicationWebviewController.ts | 668 ++++++++ src/controllers/mainController.ts | 165 ++ src/reactviews/common/locConstants.ts | 64 + .../dataTierApplication.css | 0 .../dataTierApplicationForm.tsx | 899 +++++++++++ .../dataTierApplicationPage.tsx | 18 + .../dataTierApplicationSelector.ts | 14 + .../dataTierApplicationStateProvider.tsx | 34 + .../pages/DataTierApplication/index.tsx | 18 + src/sharedInterfaces/dataTierApplication.ts | 297 ++++ ...taTierApplicationWebviewController.test.ts | 1386 +++++++++++++++++ 16 files changed, 3638 insertions(+) create mode 100644 src/controllers/dataTierApplicationWebviewController.ts create mode 100644 src/reactviews/pages/DataTierApplication/dataTierApplication.css create mode 100644 src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx create mode 100644 src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx create mode 100644 src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.ts create mode 100644 src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx create mode 100644 src/reactviews/pages/DataTierApplication/index.tsx create mode 100644 src/sharedInterfaces/dataTierApplication.ts create mode 100644 test/unit/dataTierApplicationWebviewController.test.ts diff --git a/package.json b/package.json index 9fcfd0e65a..3059d914fa 100644 --- a/package.json +++ b/package.json @@ -542,6 +542,11 @@ "when": "view == objectExplorer && viewItem =~ /\\btype=(disconnectedServer|Server|Database)\\b/", "group": "2_MSSQL_serverDbActions@2" }, + { + "command": "mssql.dataTierApplication", + "when": "view == objectExplorer && viewItem =~ /\\btype=(disconnectedServer|Server|Database)\\b/", + "group": "2_MSSQL_serverDbActions@3" + }, { "command": "mssql.scriptSelect", "when": "view == objectExplorer && viewItem =~ /\\btype=(Table|View)\\b/", @@ -1023,6 +1028,32 @@ "title": "%mssql.scriptAlter%", "category": "MS SQL" }, + { + "command": "mssql.dataTierApplication", + "title": "%mssql.dataTierApplication%", + "category": "MS SQL", + "icon": "$(database)" + }, + { + "command": "mssql.deployDacpac", + "title": "%mssql.deployDacpac%", + "category": "MS SQL" + }, + { + "command": "mssql.extractDacpac", + "title": "%mssql.extractDacpac%", + "category": "MS SQL" + }, + { + "command": "mssql.importBacpac", + "title": "%mssql.importBacpac%", + "category": "MS SQL" + }, + { + "command": "mssql.exportBacpac", + "title": "%mssql.exportBacpac%", + "category": "MS SQL" + }, { "command": "mssql.openQueryHistory", "title": "%mssql.openQueryHistory%", diff --git a/package.nls.json b/package.nls.json index 9f1a340663..d3accc4f42 100644 --- a/package.nls.json +++ b/package.nls.json @@ -15,6 +15,11 @@ "mssql.scriptDelete": "Script as Drop", "mssql.scriptExecute": "Script as Execute", "mssql.scriptAlter": "Script as Alter", + "mssql.dataTierApplication": "Data-tier Application", + "mssql.deployDacpac": "Deploy DACPAC", + "mssql.extractDacpac": "Extract DACPAC", + "mssql.importBacpac": "Import BACPAC", + "mssql.exportBacpac": "Export BACPAC", "mssql.openQueryHistory": "Open Query", "mssql.runQueryHistory": "Run Query", "mssql.deleteQueryHistory": "Delete", diff --git a/scripts/bundle-reactviews.js b/scripts/bundle-reactviews.js index 5cd89caa90..d4aea98b8f 100644 --- a/scripts/bundle-reactviews.js +++ b/scripts/bundle-reactviews.js @@ -17,6 +17,7 @@ const config = { addFirewallRule: "src/reactviews/pages/AddFirewallRule/index.tsx", connectionDialog: "src/reactviews/pages/ConnectionDialog/index.tsx", connectionGroup: "src/reactviews/pages/ConnectionGroup/index.tsx", + dataTierApplication: "src/reactviews/pages/DataTierApplication/index.tsx", deployment: "src/reactviews/pages/Deployment/index.tsx", executionPlan: "src/reactviews/pages/ExecutionPlan/index.tsx", tableDesigner: "src/reactviews/pages/TableDesigner/index.tsx", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 7e74bca2fa..5344723801 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -51,6 +51,11 @@ export const cmdCommandPaletteQueryHistory = "mssql.commandPaletteQueryHistory"; export const cmdNewQuery = "mssql.newQuery"; export const cmdSchemaCompare = "mssql.schemaCompare"; export const cmdSchemaCompareOpenFromCommandPalette = "mssql.schemaCompareOpenFromCommandPalette"; +export const cmdDataTierApplication = "mssql.dataTierApplication"; +export const cmdDeployDacpac = "mssql.deployDacpac"; +export const cmdExtractDacpac = "mssql.extractDacpac"; +export const cmdImportBacpac = "mssql.importBacpac"; +export const cmdExportBacpac = "mssql.exportBacpac"; export const cmdPublishDatabaseProject = "mssql.publishDatabaseProject"; export const cmdManageConnectionProfiles = "mssql.manageProfiles"; export const cmdClearPooledConnections = "mssql.clearPooledConnections"; diff --git a/src/constants/locConstants.ts b/src/constants/locConstants.ts index d9cc469906..6d3c82b18f 100644 --- a/src/constants/locConstants.ts +++ b/src/constants/locConstants.ts @@ -1987,3 +1987,36 @@ export class ConnectionGroup { }); }; } + +export class DataTierApplication { + public static Title = l10n.t("Data-tier Application"); + public static FilePathRequired = l10n.t("File path is required"); + public static FileNotFound = l10n.t("File not found"); + public static InvalidFileExtension = l10n.t( + "Invalid file extension. Expected .dacpac or .bacpac", + ); + public static DirectoryNotFound = l10n.t("Directory not found"); + public static FileAlreadyExists = l10n.t( + "File already exists. It will be overwritten if you continue", + ); + public static DatabaseNameRequired = l10n.t("Database name is required"); + public static InvalidDatabaseName = l10n.t( + 'Database name contains invalid characters. Avoid using: < > * ? " / \\ |', + ); + public static DatabaseNameTooLong = l10n.t( + "Database name is too long. Maximum length is 128 characters", + ); + public static DatabaseAlreadyExists = l10n.t( + "A database with this name already exists on the server", + ); + public static DatabaseNotFound = l10n.t("Database not found on the server"); + public static ValidationFailed = l10n.t("Validation failed. Please check your inputs"); + public static DeployToExistingWarning = l10n.t("Deploy to Existing Database"); + public static DeployToExistingMessage = l10n.t( + "You are about to deploy to an existing database. This operation will make permanent changes to the database schema and may result in data loss. Do you want to continue?", + ); + public static DeployToExistingConfirm = l10n.t("Deploy"); + public static Cancel = l10n.t("Cancel"); + public static Select = l10n.t("Select"); + public static Save = l10n.t("Save"); +} diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts new file mode 100644 index 0000000000..17b9d02cb3 --- /dev/null +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -0,0 +1,668 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as path from "path"; +import * as fs from "fs"; +import ConnectionManager from "./connectionManager"; +import { DacFxService } from "../services/dacFxService"; +import { IConnectionProfile } from "../models/interfaces"; +import * as vscodeMssql from "vscode-mssql"; +import { ReactWebviewPanelController } from "./reactWebviewPanelController"; +import VscodeWrapper from "./vscodeWrapper"; +import * as LocConstants from "../constants/locConstants"; +import { + BrowseInputFileWebviewRequest, + BrowseOutputFileWebviewRequest, + CancelDataTierApplicationWebviewNotification, + ConfirmDeployToExistingWebviewRequest, + ConnectionProfile, + ConnectToServerWebviewRequest, + DataTierApplicationResult, + DataTierApplicationWebviewState, + DataTierOperationType, + DeployDacpacParams, + DeployDacpacWebviewRequest, + ExportBacpacParams, + ExportBacpacWebviewRequest, + ExtractDacpacParams, + ExtractDacpacWebviewRequest, + ImportBacpacParams, + ImportBacpacWebviewRequest, + ListConnectionsWebviewRequest, + ListDatabasesWebviewRequest, + ValidateDatabaseNameWebviewRequest, + ValidateFilePathWebviewRequest, +} from "../sharedInterfaces/dataTierApplication"; +import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; +import { ListDatabasesRequest } from "../models/contracts/connection"; + +/** + * Controller for the Data-tier Application webview + * Manages DACPAC and BACPAC operations (Deploy, Extract, Import, Export) + */ +export class DataTierApplicationWebviewController extends ReactWebviewPanelController< + DataTierApplicationWebviewState, + void, + DataTierApplicationResult +> { + private _ownerUri: string; + + constructor( + context: vscode.ExtensionContext, + vscodeWrapper: VscodeWrapper, + private connectionManager: ConnectionManager, + private dacFxService: DacFxService, + initialState: DataTierApplicationWebviewState, + ownerUri: string, + ) { + super(context, vscodeWrapper, "dataTierApplication", "dataTierApplication", initialState, { + title: LocConstants.DataTierApplication.Title, + viewColumn: vscode.ViewColumn.Active, + iconPath: { + dark: vscode.Uri.joinPath(context.extensionUri, "media", "database_dark.svg"), + light: vscode.Uri.joinPath(context.extensionUri, "media", "database_light.svg"), + }, + preserveFocus: true, + }); + + this._ownerUri = ownerUri; + this.registerRpcHandlers(); + } + + /** + * Registers all RPC handlers for webview communication + */ + private registerRpcHandlers(): void { + // Deploy DACPAC request handler + this.onRequest(DeployDacpacWebviewRequest.type, async (params: DeployDacpacParams) => { + return await this.handleDeployDacpac(params); + }); + + // Extract DACPAC request handler + this.onRequest(ExtractDacpacWebviewRequest.type, async (params: ExtractDacpacParams) => { + return await this.handleExtractDacpac(params); + }); + + // Import BACPAC request handler + this.onRequest(ImportBacpacWebviewRequest.type, async (params: ImportBacpacParams) => { + return await this.handleImportBacpac(params); + }); + + // Export BACPAC request handler + this.onRequest(ExportBacpacWebviewRequest.type, async (params: ExportBacpacParams) => { + return await this.handleExportBacpac(params); + }); + + // Validate file path request handler + this.onRequest( + ValidateFilePathWebviewRequest.type, + async (params: { filePath: string; shouldExist: boolean }) => { + return this.validateFilePath(params.filePath, params.shouldExist); + }, + ); + + // List databases request handler + this.onRequest(ListDatabasesWebviewRequest.type, async (params: { ownerUri: string }) => { + if (!params.ownerUri || params.ownerUri.trim() === "") { + this.logger.error("Cannot list databases: ownerUri is empty"); + return { databases: [] }; + } + return await this.listDatabases(params.ownerUri); + }); + + // Validate database name request handler + this.onRequest( + ValidateDatabaseNameWebviewRequest.type, + async (params: { + databaseName: string; + ownerUri: string; + shouldNotExist: boolean; + operationType?: DataTierOperationType; + }) => { + if (!params.ownerUri || params.ownerUri.trim() === "") { + this.logger.error("Cannot validate database name: ownerUri is empty"); + return { + isValid: false, + errorMessage: + "No active connection. Please ensure you are connected to a SQL Server instance.", + }; + } + return await this.validateDatabaseName( + params.databaseName, + params.ownerUri, + params.shouldNotExist, + params.operationType, + ); + }, + ); + + // List connections request handler + this.onRequest(ListConnectionsWebviewRequest.type, async () => { + return await this.listConnections(); + }); + + // Connect to server request handler + this.onRequest( + ConnectToServerWebviewRequest.type, + async (params: { profileId: string }) => { + return await this.connectToServer(params.profileId); + }, + ); + + // Browse for input file (DACPAC or BACPAC) request handler + this.onRequest( + BrowseInputFileWebviewRequest.type, + async (params: { fileExtension: string }) => { + const fileUri = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + openLabel: LocConstants.DataTierApplication.Select, + filters: { + [`${params.fileExtension.toUpperCase()} Files`]: [params.fileExtension], + }, + }); + + if (!fileUri || fileUri.length === 0) { + return { filePath: undefined }; + } + + return { filePath: fileUri[0].fsPath }; + }, + ); + + // Browse for output file (DACPAC or BACPAC) request handler + this.onRequest( + BrowseOutputFileWebviewRequest.type, + async (params: { fileExtension: string; defaultFileName?: string }) => { + const defaultFileName = + params.defaultFileName || `database.${params.fileExtension}`; + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri; + const defaultUri = workspaceFolder + ? vscode.Uri.joinPath(workspaceFolder, defaultFileName) + : vscode.Uri.file(path.join(require("os").homedir(), defaultFileName)); + + const fileUri = await vscode.window.showSaveDialog({ + defaultUri: defaultUri, + saveLabel: LocConstants.DataTierApplication.Save, + filters: { + [`${params.fileExtension.toUpperCase()} Files`]: [params.fileExtension], + }, + }); + + if (!fileUri) { + return { filePath: undefined }; + } + + return { filePath: fileUri.fsPath }; + }, + ); + + // Confirm deploy to existing database request handler + this.onRequest(ConfirmDeployToExistingWebviewRequest.type, async () => { + const result = await this.vscodeWrapper.showWarningMessageAdvanced( + LocConstants.DataTierApplication.DeployToExistingMessage, + { modal: true }, + [LocConstants.DataTierApplication.DeployToExistingConfirm], + ); + + return { + confirmed: result === LocConstants.DataTierApplication.DeployToExistingConfirm, + }; + }); + + // Cancel operation notification handler + this.onNotification(CancelDataTierApplicationWebviewNotification.type, () => { + this.dialogResult.resolve(undefined); + this.panel.dispose(); + }); + } + + /** + * Handles deploying a DACPAC file to a database + */ + private async handleDeployDacpac( + params: DeployDacpacParams, + ): Promise { + try { + const result = await this.dacFxService.deployDacpac( + params.packageFilePath, + params.databaseName, + !params.isNewDatabase, // upgradeExisting + params.ownerUri, + TaskExecutionMode.execute, + ); + + const appResult: DataTierApplicationResult = { + success: result.success, + errorMessage: result.errorMessage, + operationId: result.operationId, + }; + + if (result.success) { + this.dialogResult.resolve(appResult); + // Don't dispose immediately to allow user to see success message + } + + return appResult; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + errorMessage: errorMessage, + }; + } + } + + /** + * Handles extracting a DACPAC file from a database + */ + private async handleExtractDacpac( + params: ExtractDacpacParams, + ): Promise { + try { + const result = await this.dacFxService.extractDacpac( + params.databaseName, + params.packageFilePath, + params.applicationName, + params.applicationVersion, + params.ownerUri, + TaskExecutionMode.execute, + ); + + const appResult: DataTierApplicationResult = { + success: result.success, + errorMessage: result.errorMessage, + operationId: result.operationId, + }; + + if (result.success) { + this.dialogResult.resolve(appResult); + } + + return appResult; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + errorMessage: errorMessage, + }; + } + } + + /** + * Handles importing a BACPAC file to create a new database + */ + private async handleImportBacpac( + params: ImportBacpacParams, + ): Promise { + try { + const result = await this.dacFxService.importBacpac( + params.packageFilePath, + params.databaseName, + params.ownerUri, + TaskExecutionMode.execute, + ); + + const appResult: DataTierApplicationResult = { + success: result.success, + errorMessage: result.errorMessage, + operationId: result.operationId, + }; + + if (result.success) { + this.dialogResult.resolve(appResult); + } + + return appResult; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + errorMessage: errorMessage, + }; + } + } + + /** + * Handles exporting a database to a BACPAC file + */ + private async handleExportBacpac( + params: ExportBacpacParams, + ): Promise { + try { + const result = await this.dacFxService.exportBacpac( + params.databaseName, + params.packageFilePath, + params.ownerUri, + TaskExecutionMode.execute, + ); + + const appResult: DataTierApplicationResult = { + success: result.success, + errorMessage: result.errorMessage, + operationId: result.operationId, + }; + + if (result.success) { + this.dialogResult.resolve(appResult); + } + + return appResult; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + success: false, + errorMessage: errorMessage, + }; + } + } + + /** + * Validates a file path + */ + private validateFilePath( + filePath: string, + shouldExist: boolean, + ): { isValid: boolean; errorMessage?: string } { + if (!filePath || filePath.trim() === "") { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.FilePathRequired, + }; + } + + const fileExists = fs.existsSync(filePath); + + if (shouldExist && !fileExists) { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.FileNotFound, + }; + } + + const extension = path.extname(filePath).toLowerCase(); + if (extension !== ".dacpac" && extension !== ".bacpac") { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.InvalidFileExtension, + }; + } + + if (!shouldExist) { + // Check if the directory exists and is writable + const directory = path.dirname(filePath); + if (!fs.existsSync(directory)) { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.DirectoryNotFound, + }; + } + + // Check if file already exists (for output files) + if (fileExists) { + // This is just a warning - the operation can continue with user confirmation + return { + isValid: true, + errorMessage: LocConstants.DataTierApplication.FileAlreadyExists, + }; + } + } + + return { isValid: true }; + } + + /** + * Lists databases on the connected server + */ + private async listDatabases(ownerUri: string): Promise<{ databases: string[] }> { + try { + const result = await this.connectionManager.client.sendRequest( + ListDatabasesRequest.type, + { ownerUri: ownerUri }, + ); + + return { databases: result.databaseNames || [] }; + } catch (error) { + this.logger.error(`Failed to list databases: ${error}`); + return { databases: [] }; + } + } + + /** + * Lists all available connections (recent and active) + */ + private async listConnections(): Promise<{ connections: ConnectionProfile[] }> { + try { + const connections: ConnectionProfile[] = []; + + // Get recently used connections from connection store + const recentConnections = + this.connectionManager.connectionStore.getRecentlyUsedConnections(); + + // Get active connections + const activeConnections = this.connectionManager.activeConnections; + + // Build the connection profile list + for (const conn of recentConnections) { + const profile = conn as IConnectionProfile; + const displayName = this.buildConnectionDisplayName(profile); + const profileId = profile.id || `${profile.server}_${profile.database || ""}`; + + // Check if this connection is active + const isConnected = Object.values(activeConnections).some( + (activeConn) => + activeConn.credentials.server === profile.server && + (activeConn.credentials.database === profile.database || + (!activeConn.credentials.database && !profile.database)), + ); + + connections.push({ + displayName, + server: profile.server, + database: profile.database, + authenticationType: this.getAuthenticationTypeString( + profile.authenticationType, + ), + userName: profile.user, + isConnected, + profileId, + }); + } + + return { connections }; + } catch (error) { + this.logger.error(`Failed to list connections: ${error}`); + return { connections: [] }; + } + } + + /** + * Connects to a server using the specified profile ID + */ + private async connectToServer( + profileId: string, + ): Promise<{ ownerUri: string; isConnected: boolean; errorMessage?: string }> { + try { + // Find the profile in recent connections + const recentConnections = + this.connectionManager.connectionStore.getRecentlyUsedConnections(); + const profile = recentConnections.find((conn: vscodeMssql.IConnectionInfo) => { + const connProfile = conn as IConnectionProfile; + const connId = connProfile.id || `${conn.server}_${conn.database || ""}`; + return connId === profileId; + }) as IConnectionProfile | undefined; + + if (!profile) { + return { + ownerUri: "", + isConnected: false, + errorMessage: "Connection profile not found", + }; + } + + // Check if already connected + let ownerUri = this.connectionManager.getUriForConnection(profile); + const existingConnection = + ownerUri && this.connectionManager.activeConnections[ownerUri]; + + if (existingConnection) { + return { + ownerUri, + isConnected: true, + }; + } + + // Generate a new ownerUri if we don't have one (for new connections) + // Pass empty string to let connect() generate the URI + const result = await this.connectionManager.connect("", profile); + + if (result) { + // Get the actual ownerUri that was used for the connection + ownerUri = this.connectionManager.getUriForConnection(profile); + return { + ownerUri, + isConnected: true, + }; + } else { + return { + ownerUri: "", + isConnected: false, + errorMessage: "Failed to connect to server", + }; + } + } catch (error) { + this.logger.error(`Failed to connect to server: ${error}`); + const errorMessage = error instanceof Error ? error.message : String(error); + return { + ownerUri: "", + isConnected: false, + errorMessage: `Connection failed: ${errorMessage}`, + }; + } + } + + /** + * Builds a display name for a connection profile + */ + private buildConnectionDisplayName(profile: IConnectionProfile): string { + let displayName = profile.profileName || profile.server; + if (profile.database) { + displayName += ` (${profile.database})`; + } + if (profile.user) { + displayName += ` - ${profile.user}`; + } + return displayName; + } + + /** + * Gets a string representation of the authentication type + */ + private getAuthenticationTypeString(authType: number | string | undefined): string { + switch (authType) { + case 1: + return "Integrated"; + case 2: + return "SQL Login"; + case 3: + return "Azure MFA"; + default: + return "Unknown"; + } + } + + /** + * Validates a database name + */ + private async validateDatabaseName( + databaseName: string, + ownerUri: string, + shouldNotExist: boolean, + operationType?: DataTierOperationType, + ): Promise<{ isValid: boolean; errorMessage?: string }> { + if (!databaseName || databaseName.trim() === "") { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.DatabaseNameRequired, + }; + } + + // Check for invalid characters + const invalidChars = /[<>*?"/\\|]/; + if (invalidChars.test(databaseName)) { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.InvalidDatabaseName, + }; + } + + // Check length (SQL Server max identifier length is 128) + if (databaseName.length > 128) { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.DatabaseNameTooLong, + }; + } + + // Check if database exists + try { + const result = await this.connectionManager.client.sendRequest( + ListDatabasesRequest.type, + { ownerUri: ownerUri }, + ); + + const databases = result.databaseNames || []; + const exists = databases.some((db) => db.toLowerCase() === databaseName.toLowerCase()); + + // For Deploy operations, always warn if database exists to trigger confirmation + // This ensures confirmation dialog is shown in both cases: + // 1. User selected "New Database" but database already exists (shouldNotExist=true) + // 2. User selected "Existing Database" and selected existing database (shouldNotExist=true) + if (operationType === DataTierOperationType.Deploy && exists) { + return { + isValid: true, // Allow the operation but with a warning + errorMessage: LocConstants.DataTierApplication.DatabaseAlreadyExists, + }; + } + + // For new database operations (Import), database should not exist + if (shouldNotExist && exists) { + return { + isValid: true, // Allow the operation but with a warning + errorMessage: LocConstants.DataTierApplication.DatabaseAlreadyExists, + }; + } + + // For Extract/Export operations, database must exist + if (!shouldNotExist && !exists) { + return { + isValid: false, + errorMessage: LocConstants.DataTierApplication.DatabaseNotFound, + }; + } + + return { isValid: true }; + } catch (error) { + const errorMessage = + error instanceof Error + ? `Failed to validate database name: ${error.message}` + : LocConstants.DataTierApplication.ValidationFailed; + this.logger.error(errorMessage); + return { + isValid: false, + errorMessage: errorMessage, + }; + } + } + + /** + * Gets the owner URI for the current connection + */ + public get ownerUri(): string { + return this._ownerUri; + } +} diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 15b1afb022..7b809a8fd5 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -47,6 +47,11 @@ import { ActivityStatus, TelemetryActions, TelemetryViews } from "../sharedInter import { TableDesignerService } from "../services/tableDesignerService"; import { TableDesignerWebviewController } from "../tableDesigner/tableDesignerWebviewController"; import { ConnectionDialogWebviewController } from "../connectionconfig/connectionDialogWebviewController"; +import { DataTierApplicationWebviewController } from "./dataTierApplicationWebviewController"; +import { + DataTierApplicationWebviewState, + DataTierOperationType, +} from "../sharedInterfaces/dataTierApplication"; import { ObjectExplorerFilter } from "../objectExplorer/objectExplorerFilter"; import { DatabaseObjectSearchService, @@ -1764,6 +1769,166 @@ export default class MainController implements vscode.Disposable { ), ); + // Data-tier Application - Main command + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdDataTierApplication, + async (node?: TreeNodeInfo) => { + const connectionProfile = node?.connectionProfile; + const ownerUri = connectionProfile + ? this._connectionMgr.getUriForConnection(connectionProfile) + : ""; + const serverName = connectionProfile?.server || ""; + const databaseName = connectionProfile?.database || ""; + + const initialState: DataTierApplicationWebviewState = { + ownerUri, + serverName, + databaseName, + operationType: undefined, + }; + + const controller = new DataTierApplicationWebviewController( + this._context, + this._vscodeWrapper, + this._connectionMgr, + this.dacFxService, + initialState, + ownerUri, + ); + await controller.revealToForeground(); + }, + ), + ); + + // Data-tier Application - Deploy DACPAC + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdDeployDacpac, + async (node?: TreeNodeInfo) => { + const connectionProfile = node?.connectionProfile; + const ownerUri = connectionProfile + ? this._connectionMgr.getUriForConnection(connectionProfile) + : ""; + const serverName = connectionProfile?.server || ""; + const databaseName = connectionProfile?.database || ""; + + const initialState: DataTierApplicationWebviewState = { + ownerUri, + serverName, + databaseName, + operationType: DataTierOperationType.Deploy, + }; + + const controller = new DataTierApplicationWebviewController( + this._context, + this._vscodeWrapper, + this._connectionMgr, + this.dacFxService, + initialState, + ownerUri, + ); + await controller.revealToForeground(); + }, + ), + ); + + // Data-tier Application - Extract DACPAC + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdExtractDacpac, + async (node?: TreeNodeInfo) => { + const connectionProfile = node?.connectionProfile; + const ownerUri = connectionProfile + ? this._connectionMgr.getUriForConnection(connectionProfile) + : ""; + const serverName = connectionProfile?.server || ""; + const databaseName = connectionProfile?.database || ""; + + const initialState: DataTierApplicationWebviewState = { + ownerUri, + serverName, + databaseName, + operationType: DataTierOperationType.Extract, + }; + + const controller = new DataTierApplicationWebviewController( + this._context, + this._vscodeWrapper, + this._connectionMgr, + this.dacFxService, + initialState, + ownerUri, + ); + await controller.revealToForeground(); + }, + ), + ); + + // Data-tier Application - Import BACPAC + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdImportBacpac, + async (node?: TreeNodeInfo) => { + const connectionProfile = node?.connectionProfile; + const ownerUri = connectionProfile + ? this._connectionMgr.getUriForConnection(connectionProfile) + : ""; + const serverName = connectionProfile?.server || ""; + const databaseName = connectionProfile?.database || ""; + + const initialState: DataTierApplicationWebviewState = { + ownerUri, + serverName, + databaseName, + operationType: DataTierOperationType.Import, + }; + + const controller = new DataTierApplicationWebviewController( + this._context, + this._vscodeWrapper, + this._connectionMgr, + this.dacFxService, + initialState, + ownerUri, + ); + await controller.revealToForeground(); + }, + ), + ); + + // Data-tier Application - Export BACPAC + this._context.subscriptions.push( + vscode.commands.registerCommand( + Constants.cmdExportBacpac, + async (node?: TreeNodeInfo) => { + const connectionProfile = node?.connectionProfile; + const ownerUri = connectionProfile + ? this._connectionMgr.getUriForConnection(connectionProfile) + : ""; + const serverName = connectionProfile?.server || ""; + const databaseName = connectionProfile?.database || ""; + + const initialState: DataTierApplicationWebviewState = { + ownerUri, + serverName, + databaseName, + operationType: DataTierOperationType.Export, + }; + + const controller = new DataTierApplicationWebviewController( + this._context, + this._vscodeWrapper, + this._connectionMgr, + this.dacFxService, + initialState, + ownerUri, + ); + await controller.revealToForeground(); + }, + ), + ); + // Copy object name command this._context.subscriptions.push( vscode.commands.registerCommand( diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index 72f37a7e1d..20a5c870f3 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -1053,6 +1053,70 @@ export class LocConstants { passwordsDoNotMatch: l10n.t("Passwords do not match"), }; } + + public get dataTierApplication() { + return { + title: l10n.t("Data-tier Application"), + subtitle: l10n.t( + "Deploy, extract, import, or export data-tier applications on the selected database", + ), + operationLabel: l10n.t("Operation"), + selectOperation: l10n.t("Select an operation"), + serverLabel: l10n.t("Server"), + selectServer: l10n.t("Select a server"), + noConnectionsAvailable: l10n.t( + "No connections available. Please create a connection first.", + ), + connectingToServer: l10n.t("Connecting to server..."), + connectionFailed: l10n.t("Failed to connect to server"), + deployDacpac: l10n.t("Deploy DACPAC"), + extractDacpac: l10n.t("Extract DACPAC"), + importBacpac: l10n.t("Import BACPAC"), + exportBacpac: l10n.t("Export BACPAC"), + deployDescription: l10n.t("Deploy a DACPAC to create or update a database"), + extractDescription: l10n.t("Extract a DACPAC from an existing database"), + importDescription: l10n.t("Import a BACPAC to create a new database"), + exportDescription: l10n.t("Export a BACPAC from an existing database"), + packageFileLabel: l10n.t("Package file"), + outputFileLabel: l10n.t("Output file"), + selectPackageFile: l10n.t("Select a DACPAC or BACPAC file"), + selectOutputFile: l10n.t("Enter the path for the output file"), + browse: l10n.t("Browse..."), + targetDatabaseLabel: l10n.t("Target Database"), + sourceDatabaseLabel: l10n.t("Source Database"), + databaseNameLabel: l10n.t("Database Name"), + newDatabase: l10n.t("New Database"), + existingDatabase: l10n.t("Existing Database"), + selectDatabase: l10n.t("Select a database"), + enterDatabaseName: l10n.t("Enter database name"), + applicationNameLabel: l10n.t("Application Name"), + enterApplicationName: l10n.t("Enter application name"), + applicationVersionLabel: l10n.t("Application Version"), + cancel: l10n.t("Cancel"), + execute: l10n.t("Execute"), + filePathRequired: l10n.t("File path is required"), + invalidFile: l10n.t("Invalid file"), + databaseNameRequired: l10n.t("Database name is required"), + invalidDatabase: l10n.t("Invalid database"), + validationFailed: l10n.t("Validation failed"), + deployingDacpac: l10n.t("Deploying DACPAC..."), + extractingDacpac: l10n.t("Extracting DACPAC..."), + importingBacpac: l10n.t("Importing BACPAC..."), + exportingBacpac: l10n.t("Exporting BACPAC..."), + operationFailed: l10n.t("Operation failed"), + unexpectedError: l10n.t("An unexpected error occurred"), + deploySuccess: l10n.t("DACPAC deployed successfully"), + extractSuccess: l10n.t("DACPAC extracted successfully"), + importSuccess: l10n.t("BACPAC imported successfully"), + exportSuccess: l10n.t("BACPAC exported successfully"), + deployToExistingWarning: l10n.t("Deploy to Existing Database"), + deployToExistingMessage: l10n.t( + "You are about to deploy to an existing database. This operation will make permanent changes to the database schema and may result in data loss. Do you want to continue?", + ), + deployToExistingConfirm: l10n.t("Deploy"), + databaseAlreadyExists: l10n.t("A database with this name already exists on the server"), + }; + } } export let locConstants = LocConstants.getInstance(); diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplication.css b/src/reactviews/pages/DataTierApplication/dataTierApplication.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx new file mode 100644 index 0000000000..b93d2c8ee1 --- /dev/null +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -0,0 +1,899 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Button, + Dropdown, + Field, + Input, + Label, + makeStyles, + MessageBar, + MessageBarBody, + Option, + Radio, + RadioGroup, + Spinner, + tokens, +} from "@fluentui/react-components"; +import { FolderOpen20Regular, DatabaseArrowRight20Regular } from "@fluentui/react-icons"; +import { useState, useEffect, useContext } from "react"; +import { + BrowseInputFileWebviewRequest, + BrowseOutputFileWebviewRequest, + ConnectionProfile, + ConnectToServerWebviewRequest, + DataTierOperationType, + DeployDacpacWebviewRequest, + ExtractDacpacWebviewRequest, + ImportBacpacWebviewRequest, + ExportBacpacWebviewRequest, + ListConnectionsWebviewRequest, + ValidateFilePathWebviewRequest, + ListDatabasesWebviewRequest, + ValidateDatabaseNameWebviewRequest, + CancelDataTierApplicationWebviewNotification, + ConfirmDeployToExistingWebviewRequest, +} from "../../../sharedInterfaces/dataTierApplication"; +import { DataTierApplicationContext } from "./dataTierApplicationStateProvider"; +import { useDataTierApplicationSelector } from "./dataTierApplicationSelector"; +import { locConstants } from "../../common/locConstants"; + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + width: "100%", + maxHeight: "100vh", + overflowY: "auto", + padding: "10px", + }, + formContainer: { + display: "flex", + flexDirection: "column", + width: "700px", + maxWidth: "calc(100% - 20px)", + gap: "16px", + }, + title: { + fontSize: tokens.fontSizeBase500, + fontWeight: tokens.fontWeightSemibold, + marginBottom: "8px", + }, + description: { + fontSize: tokens.fontSizeBase300, + color: tokens.colorNeutralForeground2, + marginBottom: "16px", + }, + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, + fileInputGroup: { + display: "flex", + gap: "8px", + alignItems: "flex-end", + }, + fileInput: { + flexGrow: 1, + }, + radioGroup: { + display: "flex", + flexDirection: "column", + gap: "8px", + }, + actions: { + display: "flex", + gap: "8px", + justifyContent: "flex-end", + marginTop: "16px", + paddingTop: "16px", + borderTop: `1px solid ${tokens.colorNeutralStroke2}`, + }, + progressContainer: { + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "12px", + backgroundColor: tokens.colorNeutralBackground3, + borderRadius: tokens.borderRadiusMedium, + }, + warningMessage: { + marginTop: "8px", + }, +}); + +export const DataTierApplicationForm = () => { + const classes = useStyles(); + const context = useContext(DataTierApplicationContext); + + // State from the controller + const initialOperationType = useDataTierApplicationSelector((state) => state.operationType); + const initialOwnerUri = useDataTierApplicationSelector((state) => state.ownerUri); + const initialServerName = useDataTierApplicationSelector((state) => state.serverName); + const initialDatabaseName = useDataTierApplicationSelector((state) => state.databaseName); + + // Local state + const [operationType, setOperationType] = useState( + initialOperationType || DataTierOperationType.Deploy, + ); + const [filePath, setFilePath] = useState(""); + const [databaseName, setDatabaseName] = useState(initialDatabaseName || ""); + const [isNewDatabase, setIsNewDatabase] = useState(true); + const [availableDatabases, setAvailableDatabases] = useState([]); + const [applicationName, setApplicationName] = useState(""); + const [applicationVersion, setApplicationVersion] = useState("1.0.0"); + const [isOperationInProgress, setIsOperationInProgress] = useState(false); + const [progressMessage, setProgressMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [successMessage, setSuccessMessage] = useState(""); + const [validationErrors, setValidationErrors] = useState>({}); + const [availableConnections, setAvailableConnections] = useState([]); + const [selectedProfileId, setSelectedProfileId] = useState(""); + const [ownerUri, setOwnerUri] = useState(initialOwnerUri || ""); + const [isConnecting, setIsConnecting] = useState(false); + + // Load available connections when component mounts + useEffect(() => { + void loadConnections(); + }, []); + + // Load available databases when server or operation changes + useEffect(() => { + if ( + ownerUri && + (operationType === DataTierOperationType.Deploy || + operationType === DataTierOperationType.Extract || + operationType === DataTierOperationType.Export) + ) { + void loadDatabases(); + } + }, [operationType, ownerUri]); + + const loadConnections = async () => { + try { + const result = await context?.extensionRpc?.sendRequest( + ListConnectionsWebviewRequest.type, + undefined, + ); + if (result?.connections) { + setAvailableConnections(result.connections); + + // If we have initial server/database from Object Explorer, find and select the matching connection + if (initialServerName && result.connections.length > 0) { + // Match by server and database (or server only if database is not specified) + const matchingConnection = result.connections.find((conn) => { + const serverMatches = conn.server === initialServerName; + const databaseMatches = + !initialDatabaseName || + !conn.database || + conn.database === initialDatabaseName; + return serverMatches && databaseMatches; + }); + + if (matchingConnection) { + setSelectedProfileId(matchingConnection.profileId); + + // Auto-connect if not already connected + if (!matchingConnection.isConnected) { + setIsConnecting(true); + try { + const connectResult = await context?.extensionRpc?.sendRequest( + ConnectToServerWebviewRequest.type, + { profileId: matchingConnection.profileId }, + ); + + if (connectResult?.isConnected && connectResult.ownerUri) { + setOwnerUri(connectResult.ownerUri); + // Update the connection status in our list + setAvailableConnections((prev) => + prev.map((conn) => + conn.profileId === matchingConnection.profileId + ? { ...conn, isConnected: true } + : conn, + ), + ); + } else { + setErrorMessage( + connectResult?.errorMessage || + locConstants.dataTierApplication.connectionFailed, + ); + } + } catch (error) { + const errorMsg = + error instanceof Error ? error.message : String(error); + setErrorMessage( + `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, + ); + } finally { + setIsConnecting(false); + } + } else { + // Already connected, just set the ownerUri + if (initialOwnerUri) { + setOwnerUri(initialOwnerUri); + } + } + } + } + } + } catch (error) { + console.error("Failed to load connections:", error); + } + }; + + const handleServerChange = async (profileId: string) => { + setSelectedProfileId(profileId); + setErrorMessage(""); + setSuccessMessage(""); + setValidationErrors({}); + + // Find the selected connection + const selectedConnection = availableConnections.find( + (conn) => conn.profileId === profileId, + ); + + if (!selectedConnection) { + return; + } + + // If not connected, connect to the server + if (!selectedConnection.isConnected) { + setIsConnecting(true); + try { + const result = await context?.extensionRpc?.sendRequest( + ConnectToServerWebviewRequest.type, + { profileId }, + ); + + if (result?.isConnected && result.ownerUri) { + setOwnerUri(result.ownerUri); + // Update the connection status in our list + setAvailableConnections((prev) => + prev.map((conn) => + conn.profileId === profileId ? { ...conn, isConnected: true } : conn, + ), + ); + // Databases will be loaded automatically via useEffect + } else { + setErrorMessage( + result?.errorMessage || locConstants.dataTierApplication.connectionFailed, + ); + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + setErrorMessage( + `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, + ); + } finally { + setIsConnecting(false); + } + } else { + // Already connected, just need to get the ownerUri + // For now, we'll need to trigger a connection to get the ownerUri + // In a future enhancement, we could store ownerUri in the connection profile + try { + const result = await context?.extensionRpc?.sendRequest( + ConnectToServerWebviewRequest.type, + { profileId }, + ); + + if (result?.ownerUri) { + setOwnerUri(result.ownerUri); + } + } catch (error) { + console.error("Failed to get ownerUri:", error); + } + } + }; + + const loadDatabases = async () => { + try { + const result = await context?.extensionRpc?.sendRequest( + ListDatabasesWebviewRequest.type, + { ownerUri: ownerUri || "" }, + ); + if (result?.databases) { + setAvailableDatabases(result.databases); + } + } catch (error) { + console.error("Failed to load databases:", error); + } + }; + + const validateFilePath = async (path: string, shouldExist: boolean): Promise => { + if (!path) { + setValidationErrors((prev) => ({ + ...prev, + filePath: locConstants.dataTierApplication.filePathRequired, + })); + return false; + } + + try { + const result = await context?.extensionRpc?.sendRequest( + ValidateFilePathWebviewRequest.type, + { filePath: path, shouldExist }, + ); + + if (!result?.isValid) { + setValidationErrors((prev) => ({ + ...prev, + filePath: result?.errorMessage || locConstants.dataTierApplication.invalidFile, + })); + return false; + } + + // Clear error or set warning for file overwrite + if (result.errorMessage) { + setValidationErrors((prev) => ({ + ...prev, + filePath: result.errorMessage || "", // This is a warning about overwrite + })); + } else { + setValidationErrors((prev) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { filePath: _fp, ...rest } = prev; + return rest; + }); + } + return true; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : locConstants.dataTierApplication.validationFailed; + setValidationErrors((prev) => ({ + ...prev, + filePath: errorMessage, + })); + return false; + } + }; + + const validateDatabaseName = async ( + dbName: string, + shouldNotExist: boolean, + ): Promise => { + if (!dbName) { + setValidationErrors((prev) => ({ + ...prev, + databaseName: locConstants.dataTierApplication.databaseNameRequired, + })); + return false; + } + + try { + const result = await context?.extensionRpc?.sendRequest( + ValidateDatabaseNameWebviewRequest.type, + { + databaseName: dbName, + ownerUri: ownerUri || "", + shouldNotExist: shouldNotExist, + operationType: operationType, + }, + ); + + if (!result?.isValid) { + setValidationErrors((prev) => ({ + ...prev, + databaseName: + result?.errorMessage || locConstants.dataTierApplication.invalidDatabase, + })); + return false; + } + + // Clear validation errors if valid + setValidationErrors((prev) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { databaseName: _dn, ...rest } = prev; + return rest; + }); + + // If deploying to an existing database, show confirmation dialog + // This can happen in two cases: + // 1. User checked "New Database" but database already exists (shouldNotExist=true) + // 2. User unchecked "New Database" to deploy to existing (shouldNotExist=false) + if ( + operationType === DataTierOperationType.Deploy && + result.errorMessage === locConstants.dataTierApplication.databaseAlreadyExists + ) { + const confirmResult = await context?.extensionRpc?.sendRequest( + ConfirmDeployToExistingWebviewRequest.type, + undefined, + ); + + return confirmResult?.confirmed === true; + } + + return true; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : locConstants.dataTierApplication.validationFailed; + setValidationErrors((prev) => ({ + ...prev, + databaseName: errorMessage, + })); + return false; + } + }; + + const handleSubmit = async () => { + setErrorMessage(""); + setSuccessMessage(""); + setIsOperationInProgress(true); + + try { + let result; + + switch (operationType) { + case DataTierOperationType.Deploy: + if ( + !(await validateFilePath(filePath, true)) || + !(await validateDatabaseName(databaseName, isNewDatabase)) + ) { + setIsOperationInProgress(false); + return; + } + setProgressMessage(locConstants.dataTierApplication.deployingDacpac); + result = await context?.extensionRpc?.sendRequest( + DeployDacpacWebviewRequest.type, + { + packageFilePath: filePath, + databaseName, + isNewDatabase, + ownerUri: ownerUri || "", + }, + ); + break; + + case DataTierOperationType.Extract: + if ( + !(await validateFilePath(filePath, false)) || + !(await validateDatabaseName(databaseName, false)) + ) { + setIsOperationInProgress(false); + return; + } + setProgressMessage(locConstants.dataTierApplication.extractingDacpac); + result = await context?.extensionRpc?.sendRequest( + ExtractDacpacWebviewRequest.type, + { + databaseName, + packageFilePath: filePath, + applicationName, + applicationVersion, + ownerUri: ownerUri || "", + }, + ); + break; + + case DataTierOperationType.Import: + if ( + !(await validateFilePath(filePath, true)) || + !(await validateDatabaseName(databaseName, true)) + ) { + setIsOperationInProgress(false); + return; + } + setProgressMessage(locConstants.dataTierApplication.importingBacpac); + result = await context?.extensionRpc?.sendRequest( + ImportBacpacWebviewRequest.type, + { + packageFilePath: filePath, + databaseName, + ownerUri: ownerUri || "", + }, + ); + break; + + case DataTierOperationType.Export: + if ( + !(await validateFilePath(filePath, false)) || + !(await validateDatabaseName(databaseName, false)) + ) { + setIsOperationInProgress(false); + return; + } + setProgressMessage(locConstants.dataTierApplication.exportingBacpac); + result = await context?.extensionRpc?.sendRequest( + ExportBacpacWebviewRequest.type, + { + databaseName, + packageFilePath: filePath, + ownerUri: ownerUri || "", + }, + ); + break; + } + + if (result?.success) { + setSuccessMessage(getSuccessMessage(operationType)); + setProgressMessage(""); + } else { + setErrorMessage( + result?.errorMessage || locConstants.dataTierApplication.operationFailed, + ); + setProgressMessage(""); + } + } catch (error) { + setErrorMessage( + error instanceof Error + ? error.message + : locConstants.dataTierApplication.unexpectedError, + ); + setProgressMessage(""); + } finally { + setIsOperationInProgress(false); + } + }; + + const handleBrowseFile = async () => { + const fileExtension = + operationType === DataTierOperationType.Deploy || + operationType === DataTierOperationType.Extract + ? "dacpac" + : "bacpac"; + + let result: { filePath?: string } | undefined; + + if (requiresInputFile) { + // Browse for input file (Deploy or Import) + result = await context?.extensionRpc?.sendRequest(BrowseInputFileWebviewRequest.type, { + fileExtension, + }); + } else { + // Browse for output file (Extract or Export) + const defaultFileName = `${initialDatabaseName || "database"}.${fileExtension}`; + result = await context?.extensionRpc?.sendRequest(BrowseOutputFileWebviewRequest.type, { + fileExtension, + defaultFileName, + }); + } + + if (result?.filePath) { + setFilePath(result.filePath); + // Clear validation error when file is selected + const newErrors = { ...validationErrors }; + delete newErrors.filePath; + setValidationErrors(newErrors); + // Validate the selected file path + await validateFilePath(result.filePath, requiresInputFile); + } + }; + + const handleCancel = async () => { + await context?.extensionRpc?.sendNotification( + CancelDataTierApplicationWebviewNotification.type, + ); + }; + + const getSuccessMessage = (type: DataTierOperationType): string => { + switch (type) { + case DataTierOperationType.Deploy: + return locConstants.dataTierApplication.deploySuccess; + case DataTierOperationType.Extract: + return locConstants.dataTierApplication.extractSuccess; + case DataTierOperationType.Import: + return locConstants.dataTierApplication.importSuccess; + case DataTierOperationType.Export: + return locConstants.dataTierApplication.exportSuccess; + } + }; + + const getOperationDescription = (type: DataTierOperationType): string => { + switch (type) { + case DataTierOperationType.Deploy: + return locConstants.dataTierApplication.deployDescription; + case DataTierOperationType.Extract: + return locConstants.dataTierApplication.extractDescription; + case DataTierOperationType.Import: + return locConstants.dataTierApplication.importDescription; + case DataTierOperationType.Export: + return locConstants.dataTierApplication.exportDescription; + } + }; + + const isFormValid = () => { + if (!filePath || !databaseName) return false; + if ( + operationType === DataTierOperationType.Extract && + (!applicationName || !applicationVersion) + ) + return false; + return Object.keys(validationErrors).length === 0; + }; + + const requiresInputFile = + operationType === DataTierOperationType.Deploy || + operationType === DataTierOperationType.Import; + const showDatabaseTarget = operationType === DataTierOperationType.Deploy; + const showDatabaseSource = + operationType === DataTierOperationType.Extract || + operationType === DataTierOperationType.Export; + const showNewDatabase = operationType === DataTierOperationType.Import; + const showApplicationInfo = operationType === DataTierOperationType.Extract; + + return ( +
+
+
+
{locConstants.dataTierApplication.title}
+
+ {locConstants.dataTierApplication.subtitle} +
+
+ + {errorMessage && ( + + {errorMessage} + + )} + + {successMessage && ( + + {successMessage} + + )} + + {isOperationInProgress && ( +
+ +
+ )} + +
+ + { + setOperationType(data.optionValue as DataTierOperationType); + setErrorMessage(""); + setSuccessMessage(""); + setValidationErrors({}); + }} + disabled={isOperationInProgress}> + + + + + + + + +
+ +
+ + {isConnecting ? ( + + ) : ( + conn.profileId === selectedProfileId, + )?.displayName + : "" + } + selectedOptions={selectedProfileId ? [selectedProfileId] : []} + onOptionSelect={(_, data) => { + void handleServerChange(data.optionValue as string); + }} + disabled={ + isOperationInProgress || availableConnections.length === 0 + }> + {availableConnections.length === 0 ? ( + + ) : ( + availableConnections.map((conn) => ( + + )) + )} + + )} + +
+ +
+ +
+ setFilePath(data.value)} + placeholder={ + requiresInputFile + ? locConstants.dataTierApplication.selectPackageFile + : locConstants.dataTierApplication.selectOutputFile + } + disabled={isOperationInProgress} + /> + +
+
+
+ + {showDatabaseTarget && ( +
+ + setIsNewDatabase(data.value === "new")} + className={classes.radioGroup}> + + + + + {isNewDatabase ? ( + + setDatabaseName(data.value)} + placeholder={locConstants.dataTierApplication.enterDatabaseName} + disabled={isOperationInProgress} + /> + + ) : ( + + + setDatabaseName(data.optionText || "") + } + disabled={isOperationInProgress}> + {availableDatabases.map((db) => ( + + ))} + + + )} +
+ )} + + {(showDatabaseSource || showNewDatabase) && ( +
+ {showDatabaseSource ? ( + + + setDatabaseName(data.optionText || "") + } + disabled={isOperationInProgress}> + {availableDatabases.map((db) => ( + + ))} + + + ) : ( + + setDatabaseName(data.value)} + placeholder={locConstants.dataTierApplication.enterDatabaseName} + disabled={isOperationInProgress} + /> + + )} +
+ )} + + {showApplicationInfo && ( +
+ + setApplicationName(data.value)} + placeholder={locConstants.dataTierApplication.enterApplicationName} + disabled={isOperationInProgress} + /> + + + + setApplicationVersion(data.value)} + placeholder="1.0.0" + disabled={isOperationInProgress} + /> + +
+ )} + +
+ + +
+
+
+ ); +}; diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx new file mode 100644 index 0000000000..0166d9f20b --- /dev/null +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx @@ -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 { useContext } from "react"; +import { DataTierApplicationContext } from "./dataTierApplicationStateProvider"; +import { DataTierApplicationForm } from "./dataTierApplicationForm"; + +export const DataTierApplicationPage = () => { + const context = useContext(DataTierApplicationContext); + + if (!context) { + return
Loading...
; + } + + return ; +}; diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.ts b/src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.ts new file mode 100644 index 0000000000..8808b0f535 --- /dev/null +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.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 { DataTierApplicationWebviewState } from "../../../sharedInterfaces/dataTierApplication"; +import { useVscodeSelector } from "../../common/useVscodeSelector"; + +export function useDataTierApplicationSelector( + selector: (state: DataTierApplicationWebviewState) => T, + equals?: (a: T, b: T) => boolean, +) { + return useVscodeSelector(selector, equals); +} diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx new file mode 100644 index 0000000000..bcec7dc4f0 --- /dev/null +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx @@ -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 React, { createContext, ReactNode } from "react"; +import { DataTierApplicationWebviewState } from "../../../sharedInterfaces/dataTierApplication"; +import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2"; +import { WebviewRpc } from "../../common/rpc"; + +export interface DataTierApplicationReactProvider { + extensionRpc: WebviewRpc; +} + +export const DataTierApplicationContext = createContext< + DataTierApplicationReactProvider | undefined +>(undefined); + +interface DataTierApplicationProviderProps { + children: ReactNode; +} + +const DataTierApplicationStateProvider: React.FC = ({ + children, +}) => { + const { extensionRpc } = useVscodeWebview2(); + return ( + + {children} + + ); +}; + +export { DataTierApplicationStateProvider }; diff --git a/src/reactviews/pages/DataTierApplication/index.tsx b/src/reactviews/pages/DataTierApplication/index.tsx new file mode 100644 index 0000000000..96d4544cb0 --- /dev/null +++ b/src/reactviews/pages/DataTierApplication/index.tsx @@ -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 ReactDOM from "react-dom/client"; +import "../../index.css"; +import { VscodeWebviewProvider2 } from "../../common/vscodeWebviewProvider2"; +import { DataTierApplicationStateProvider } from "./dataTierApplicationStateProvider"; +import { DataTierApplicationPage } from "./dataTierApplicationPage"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/src/sharedInterfaces/dataTierApplication.ts b/src/sharedInterfaces/dataTierApplication.ts new file mode 100644 index 0000000000..b449fb6057 --- /dev/null +++ b/src/sharedInterfaces/dataTierApplication.ts @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotificationType, RequestType } from "vscode-jsonrpc/browser"; + +/** + * The type of Data-tier Application operation to perform + */ +export enum DataTierOperationType { + Deploy = "deploy", + Extract = "extract", + Import = "import", + Export = "export", +} + +/** + * Simplified connection profile for display in UI + */ +export interface ConnectionProfile { + /** + * Display name for the connection + */ + displayName: string; + /** + * Server name + */ + server: string; + /** + * Database name (if specified) + */ + database?: string; + /** + * Authentication type + */ + authenticationType: string; + /** + * User name (for SQL Auth) + */ + userName?: string; + /** + * Whether this connection is currently active + */ + isConnected: boolean; + /** + * The profile ID used to identify this connection + */ + profileId: string; +} + +/** + * The state of the Data-tier Application webview + */ +export interface DataTierApplicationWebviewState { + /** + * The currently selected operation type + */ + operationType: DataTierOperationType; + /** + * The selected DACPAC/BACPAC file path + */ + filePath?: string; + /** + * The connection owner URI + */ + ownerUri?: string; + /** + * The target/source server name + */ + serverName?: string; + /** + * The target/source database name + */ + databaseName?: string; + /** + * The currently selected connection profile ID + */ + selectedProfileId?: string; + /** + * List of available connection profiles + */ + availableConnections?: ConnectionProfile[]; + /** + * Whether to create a new database or upgrade existing (for Deploy) + */ + isNewDatabase?: boolean; + /** + * List of available databases on the server + */ + availableDatabases?: string[]; + /** + * Application name for Extract operation + */ + applicationName?: string; + /** + * Application version for Extract operation + */ + applicationVersion?: string; + /** + * Whether an operation is currently in progress + */ + isOperationInProgress?: boolean; + /** + * The current operation progress message + */ + progressMessage?: string; + /** + * Validation errors for the current form state + */ + validationErrors?: Record; +} + +/** + * Parameters for deploying a DACPAC + */ +export interface DeployDacpacParams { + packageFilePath: string; + databaseName: string; + isNewDatabase: boolean; + ownerUri: string; +} + +/** + * Parameters for extracting a DACPAC + */ +export interface ExtractDacpacParams { + databaseName: string; + packageFilePath: string; + applicationName: string; + applicationVersion: string; + ownerUri: string; +} + +/** + * Parameters for importing a BACPAC + */ +export interface ImportBacpacParams { + packageFilePath: string; + databaseName: string; + ownerUri: string; +} + +/** + * Parameters for exporting a BACPAC + */ +export interface ExportBacpacParams { + databaseName: string; + packageFilePath: string; + ownerUri: string; +} + +/** + * Result from a Data-tier Application operation + */ +export interface DataTierApplicationResult { + success: boolean; + errorMessage?: string; + operationId?: string; +} + +/** + * Request to deploy a DACPAC from the webview + */ +export namespace DeployDacpacWebviewRequest { + export const type = new RequestType( + "dataTierApplication/deployDacpac", + ); +} + +/** + * Request to extract a DACPAC from the webview + */ +export namespace ExtractDacpacWebviewRequest { + export const type = new RequestType( + "dataTierApplication/extractDacpac", + ); +} + +/** + * Request to import a BACPAC from the webview + */ +export namespace ImportBacpacWebviewRequest { + export const type = new RequestType( + "dataTierApplication/importBacpac", + ); +} + +/** + * Request to export a BACPAC from the webview + */ +export namespace ExportBacpacWebviewRequest { + export const type = new RequestType( + "dataTierApplication/exportBacpac", + ); +} + +/** + * Request to validate a file path from the webview + */ +export namespace ValidateFilePathWebviewRequest { + export const type = new RequestType< + { filePath: string; shouldExist: boolean }, + { isValid: boolean; errorMessage?: string }, + void + >("dataTierApplication/validateFilePath"); +} + +/** + * Request to list databases on a server from the webview + */ +export namespace ListDatabasesWebviewRequest { + export const type = new RequestType<{ ownerUri: string }, { databases: string[] }, void>( + "dataTierApplication/listDatabases", + ); +} + +/** + * Request to validate a database name from the webview + */ +export namespace ValidateDatabaseNameWebviewRequest { + export const type = new RequestType< + { + databaseName: string; + ownerUri: string; + shouldNotExist: boolean; + operationType?: DataTierOperationType; + }, + { isValid: boolean; errorMessage?: string }, + void + >("dataTierApplication/validateDatabaseName"); +} + +/** + * Request to list available connections from the webview + */ +export namespace ListConnectionsWebviewRequest { + export const type = new RequestType( + "dataTierApplication/listConnections", + ); +} + +/** + * Request to connect to a server from the webview + */ +export namespace ConnectToServerWebviewRequest { + export const type = new RequestType< + { profileId: string }, + { ownerUri: string; isConnected: boolean; errorMessage?: string }, + void + >("dataTierApplication/connectToServer"); +} + +/** + * Notification sent from the webview to cancel the operation + */ +export namespace CancelDataTierApplicationWebviewNotification { + export const type = new NotificationType("dataTierApplication/cancel"); +} + +/** + * Notification sent to the webview to update progress + */ +export namespace DataTierApplicationProgressNotification { + export const type = new NotificationType<{ message: string; percentage?: number }>( + "dataTierApplication/progress", + ); +} + +/** + * Request to browse for an input file (DACPAC or BACPAC) from the webview + */ +export namespace BrowseInputFileWebviewRequest { + export const type = new RequestType<{ fileExtension: string }, { filePath?: string }, void>( + "dataTierApplication/browseInputFile", + ); +} + +/** + * Request to browse for an output file (DACPAC or BACPAC) from the webview + */ +export namespace BrowseOutputFileWebviewRequest { + export const type = new RequestType< + { fileExtension: string; defaultFileName?: string }, + { filePath?: string }, + void + >("dataTierApplication/browseOutputFile"); +} + +/** + * Request to show a confirmation dialog for deploying to an existing database + */ +export namespace ConfirmDeployToExistingWebviewRequest { + export const type = new RequestType( + "dataTierApplication/confirmDeployToExisting", + ); +} diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts new file mode 100644 index 0000000000..82034fea5d --- /dev/null +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -0,0 +1,1386 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as sinon from "sinon"; +import sinonChai from "sinon-chai"; +import * as chai from "chai"; +import { expect } from "chai"; +import * as jsonRpc from "vscode-jsonrpc/node"; +import { DataTierApplicationWebviewController } from "../../src/controllers/dataTierApplicationWebviewController"; +import ConnectionManager from "../../src/controllers/connectionManager"; +import { DacFxService } from "../../src/services/dacFxService"; +import { + CancelDataTierApplicationWebviewNotification, + ConfirmDeployToExistingWebviewRequest, + ConnectToServerWebviewRequest, + DataTierApplicationResult, + DataTierOperationType, + DeployDacpacWebviewRequest, + ExportBacpacWebviewRequest, + ExtractDacpacWebviewRequest, + ImportBacpacWebviewRequest, + ListConnectionsWebviewRequest, + ListDatabasesWebviewRequest, + ValidateDatabaseNameWebviewRequest, + ValidateFilePathWebviewRequest, +} from "../../src/sharedInterfaces/dataTierApplication"; +import * as LocConstants from "../../src/constants/locConstants"; +import { + stubTelemetry, + stubVscodeWrapper, + stubWebviewConnectionRpc, + stubWebviewPanel, +} from "./utils"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import { Logger } from "../../src/models/logger"; +import * as utils from "../../src/utils/utils"; +import { DacFxResult } from "vscode-mssql"; +import { ListDatabasesRequest } from "../../src/models/contracts/connection"; +import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; +import { ConnectionStore } from "../../src/models/connectionStore"; +import * as fs from "fs"; + +chai.use(sinonChai); + +suite("DataTierApplicationWebviewController", () => { + let sandbox: sinon.SinonSandbox; + let mockContext: vscode.ExtensionContext; + let vscodeWrapperStub: sinon.SinonStubbedInstance; + let connectionManagerStub: sinon.SinonStubbedInstance; + let dacFxServiceStub: sinon.SinonStubbedInstance; + let sqlToolsClientStub: sinon.SinonStubbedInstance; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let requestHandlers: Map Promise>; + let notificationHandlers: Map void>; + let connectionStub: jsonRpc.MessageConnection; + let createWebviewPanelStub: sinon.SinonStub; + let panelStub: vscode.WebviewPanel; + let controller: DataTierApplicationWebviewController; + let fsExistsSyncStub: sinon.SinonStub; + + const ownerUri = "test-connection-uri"; + const initialState = { + operationType: DataTierOperationType.Deploy, + serverName: "test-server", + }; + + setup(() => { + sandbox = sinon.createSandbox(); + stubTelemetry(sandbox); + + const loggerStub = sandbox.createStubInstance(Logger); + sandbox.stub(Logger, "create").returns(loggerStub); + + sandbox.stub(utils, "getNonce").returns("test-nonce"); + + const connection = stubWebviewConnectionRpc(sandbox); + requestHandlers = connection.requestHandlers; + notificationHandlers = connection.notificationHandlers; + connectionStub = connection.connection; + + sandbox + .stub(jsonRpc, "createMessageConnection") + .returns(connectionStub as unknown as jsonRpc.MessageConnection); + + panelStub = stubWebviewPanel(sandbox); + createWebviewPanelStub = sandbox + .stub(vscode.window, "createWebviewPanel") + .callsFake(() => panelStub); + + mockContext = { + extensionUri: vscode.Uri.file("/tmp/ext"), + extensionPath: "/tmp/ext", + subscriptions: [], + } as unknown as vscode.ExtensionContext; + + vscodeWrapperStub = stubVscodeWrapper(sandbox); + connectionManagerStub = sandbox.createStubInstance(ConnectionManager); + dacFxServiceStub = sandbox.createStubInstance(DacFxService); + sqlToolsClientStub = sandbox.createStubInstance(SqlToolsServiceClient); + + // Set up connection manager client + sandbox.stub(connectionManagerStub, "client").get(() => sqlToolsClientStub); + + // Stub fs.existsSync + fsExistsSyncStub = sandbox.stub(fs, "existsSync"); + }); + + teardown(() => { + sandbox.restore(); + }); + + function createController(): DataTierApplicationWebviewController { + controller = new DataTierApplicationWebviewController( + mockContext, + vscodeWrapperStub, + connectionManagerStub, + dacFxServiceStub, + initialState, + ownerUri, + ); + return controller; + } + + suite("Deployment Operations", () => { + test("deploy DACPAC succeeds for new database", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "operation-123", + }; + + dacFxServiceStub.deployDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "NewDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); + const response = (await requestHandler!(params)) as DataTierApplicationResult; + + expect(dacFxServiceStub.deployDacpac).to.have.been.calledOnce; + expect(dacFxServiceStub.deployDacpac).to.have.been.calledWith( + params.packageFilePath, + params.databaseName, + false, // upgradeExisting = !isNewDatabase + params.ownerUri, + 0, // TaskExecutionMode.execute + ); + expect(response).to.deep.equal({ + success: true, + errorMessage: undefined, + operationId: "operation-123", + }); + expect(resolveSpy).to.have.been.calledOnce; + }); + + test("deploy DACPAC succeeds for existing database", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "operation-456", + }; + + dacFxServiceStub.deployDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "ExistingDatabase", + isNewDatabase: false, + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(dacFxServiceStub.deployDacpac).to.have.been.calledWith( + params.packageFilePath, + params.databaseName, + true, // upgradeExisting = !isNewDatabase + params.ownerUri, + 0, + ); + expect(response.success).to.be.true; + }); + + test("deploy DACPAC returns error on failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Deployment failed: Permission denied", + operationId: "operation-789", + }; + + dacFxServiceStub.deployDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "TestDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); + const response = await requestHandler!(params); + + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("Deployment failed: Permission denied"); + expect(resolveSpy).to.not.have.been.called; + }); + + test("deploy DACPAC handles exception", async () => { + dacFxServiceStub.deployDacpac.rejects(new Error("Network timeout")); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "TestDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("Network timeout"); + }); + }); + + suite("Extract Operations", () => { + test("extract DACPAC succeeds", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "extract-123", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const params = { + databaseName: "SourceDatabase", + packageFilePath: "C:\\output\\database.dacpac", + applicationName: "MyApp", + applicationVersion: "1.0.0", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(dacFxServiceStub.extractDacpac).to.have.been.calledOnce; + expect(dacFxServiceStub.extractDacpac).to.have.been.calledWith( + params.databaseName, + params.packageFilePath, + params.applicationName, + params.applicationVersion, + params.ownerUri, + 0, + ); + expect(response.success).to.be.true; + }); + + test("extract DACPAC returns error on failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Extraction failed: Database not found", + operationId: "extract-456", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + const params = { + databaseName: "NonExistentDatabase", + packageFilePath: "C:\\output\\database.dacpac", + applicationName: "MyApp", + applicationVersion: "1.0.0", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("Extraction failed: Database not found"); + }); + }); + + suite("Import Operations", () => { + test("import BACPAC succeeds", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "import-123", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const params = { + packageFilePath: "C:\\backup\\database.bacpac", + databaseName: "RestoredDatabase", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(dacFxServiceStub.importBacpac).to.have.been.calledOnce; + expect(dacFxServiceStub.importBacpac).to.have.been.calledWith( + params.packageFilePath, + params.databaseName, + params.ownerUri, + 0, + ); + expect(response.success).to.be.true; + }); + + test("import BACPAC returns error on failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Import failed: Corrupted BACPAC file", + operationId: "import-456", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\backup\\corrupted.bacpac", + databaseName: "TestDatabase", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("Import failed: Corrupted BACPAC file"); + }); + }); + + suite("Export Operations", () => { + test("export BACPAC succeeds", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "export-123", + }; + + dacFxServiceStub.exportBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const params = { + databaseName: "SourceDatabase", + packageFilePath: "C:\\backup\\database.bacpac", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(dacFxServiceStub.exportBacpac).to.have.been.calledOnce; + expect(dacFxServiceStub.exportBacpac).to.have.been.calledWith( + params.databaseName, + params.packageFilePath, + params.ownerUri, + 0, + ); + expect(response.success).to.be.true; + }); + + test("export BACPAC returns error on failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Export failed: Insufficient permissions", + operationId: "export-456", + }; + + dacFxServiceStub.exportBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); + const params = { + databaseName: "ProtectedDatabase", + packageFilePath: "C:\\backup\\database.bacpac", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("Export failed: Insufficient permissions"); + }); + }); + + suite("File Path Validation", () => { + test("validates existing DACPAC file", async () => { + fsExistsSyncStub.returns(true); + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const response = await requestHandler!({ + filePath: "C:\\test\\database.dacpac", + shouldExist: true, + }); + + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.be.undefined; + }); + + test("rejects non-existent file when it should exist", async () => { + fsExistsSyncStub.returns(false); + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + const response = await requestHandler!({ + filePath: "C:\\test\\missing.dacpac", + shouldExist: true, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal(LocConstants.DataTierApplication.FileNotFound); + }); + + test("rejects empty file path", async () => { + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + const response = await requestHandler!({ + filePath: "", + shouldExist: true, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.FilePathRequired, + ); + }); + + test("rejects invalid file extension", async () => { + fsExistsSyncStub.returns(true); + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + const response = await requestHandler!({ + filePath: "C:\\test\\database.txt", + shouldExist: true, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.InvalidFileExtension, + ); + }); + + test("validates output file path when directory exists", async () => { + // File doesn't exist, but directory does + fsExistsSyncStub.withArgs("C:\\output\\database.dacpac").returns(false); + fsExistsSyncStub.withArgs("C:\\output").returns(true); + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + const response = await requestHandler!({ + filePath: "C:\\output\\database.dacpac", + shouldExist: false, + }); + + expect(response.isValid).to.be.true; + }); + + test("warns when output file already exists", async () => { + // Both file and directory exist + fsExistsSyncStub.returns(true); + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + const response = await requestHandler!({ + filePath: "C:\\output\\existing.dacpac", + shouldExist: false, + }); + + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.FileAlreadyExists, + ); + }); + + test("rejects output file path when directory doesn't exist", async () => { + fsExistsSyncStub.returns(false); + createController(); + + const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); + const response = await requestHandler!({ + filePath: "C:\\nonexistent\\database.dacpac", + shouldExist: false, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.DirectoryNotFound, + ); + }); + }); + + suite("Database Operations", () => { + test("lists databases successfully", async () => { + const mockDatabases = { + databaseNames: ["master", "tempdb", "model", "msdb", "TestDB"], + }; + + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + + createController(); + + const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const response = await requestHandler!({ ownerUri: ownerUri }); + + expect(response.databases).to.deep.equal(mockDatabases.databaseNames); + expect(sqlToolsClientStub.sendRequest).to.have.been.calledWith( + ListDatabasesRequest.type, + { ownerUri: ownerUri }, + ); + }); + + test("returns empty array when list databases fails", async () => { + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .rejects(new Error("Connection failed")); + + createController(); + + const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + const response = await requestHandler!({ ownerUri: ownerUri }); + + expect(response.databases).to.be.an("array").that.is.empty; + }); + }); + + suite("Database Name Validation", () => { + test("validates non-existent database name for new database", async () => { + const mockDatabases = { + databaseNames: ["master", "tempdb", "model", "msdb"], + }; + + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "NewDatabase", + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.true; + }); + + test("allows existing database name for new database with warning", async () => { + const mockDatabases = { + databaseNames: ["master", "tempdb", "ExistingDB"], + }; + + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "ExistingDB", + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.DatabaseAlreadyExists, + ); + }); + + test("validates existing database name for extract/export", async () => { + const mockDatabases = { + databaseNames: ["master", "tempdb", "SourceDB"], + }; + + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "SourceDB", + ownerUri: ownerUri, + shouldNotExist: false, + }); + + expect(response.isValid).to.be.true; + }); + + test("rejects non-existent database name for extract/export", async () => { + const mockDatabases = { + databaseNames: ["master", "tempdb"], + }; + + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "MissingDB", + ownerUri: ownerUri, + shouldNotExist: false, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.DatabaseNotFound, + ); + }); + + test("rejects empty database name", async () => { + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "", + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.DatabaseNameRequired, + ); + }); + + test("rejects database name with invalid characters", async () => { + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "Invalid<>Database", + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.InvalidDatabaseName, + ); + }); + + test("rejects database name that is too long", async () => { + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const longName = "A".repeat(129); // Exceeds 128 character limit + const response = await requestHandler!({ + databaseName: longName, + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.DatabaseNameTooLong, + ); + }); + + test("validates database name case-insensitively with warning", async () => { + const mockDatabases = { + databaseNames: ["ExistingDB"], + }; + + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "existingdb", // Different case + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.equal( + LocConstants.DataTierApplication.DatabaseAlreadyExists, + ); + }); + + test("returns validation failed on error", async () => { + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .rejects(new Error("Network error")); + + createController(); + + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "TestDB", + ownerUri: ownerUri, + shouldNotExist: true, + }); + + expect(response.isValid).to.be.false; + // Now returns actual error message instead of generic one + expect(response.errorMessage).to.include("Failed to validate database name"); + expect(response.errorMessage).to.include("Network error"); + }); + }); + + suite("Cancel Operation", () => { + test("cancel notification resolves dialog with undefined and disposes panel", async () => { + createController(); + + const cancelHandler = notificationHandlers.get( + CancelDataTierApplicationWebviewNotification.type.method, + ); + expect(cancelHandler, "Cancel handler was not registered").to.be.a("function"); + + const resultPromise = controller.dialogResult.promise; + const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); + + (cancelHandler as () => void)(); + const resolvedValue = await resultPromise; + + expect(resolveSpy).to.have.been.calledOnceWithExactly(undefined); + expect(panelStub.dispose).to.have.been.calledOnce; + expect(resolvedValue).to.be.undefined; + }); + }); + + suite("Deploy to Existing Database Confirmation", () => { + test("confirmation dialog shows and returns confirmed=true when user clicks Deploy", async () => { + createController(); + + const confirmHandler = requestHandlers.get( + ConfirmDeployToExistingWebviewRequest.type.method, + ); + expect(confirmHandler, "Confirm handler was not registered").to.be.a("function"); + + // Mock user clicking "Deploy" button + vscodeWrapperStub.showWarningMessageAdvanced.resolves( + LocConstants.DataTierApplication.DeployToExistingConfirm, + ); + + const response = await confirmHandler!(undefined); + + expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( + LocConstants.DataTierApplication.DeployToExistingMessage, + { modal: true }, + [LocConstants.DataTierApplication.DeployToExistingConfirm], + ); + expect(response.confirmed).to.be.true; + }); + + test("confirmation dialog returns confirmed=false when user clicks Cancel", async () => { + createController(); + + const confirmHandler = requestHandlers.get( + ConfirmDeployToExistingWebviewRequest.type.method, + ); + expect(confirmHandler, "Confirm handler was not registered").to.be.a("function"); + + // Mock user clicking Cancel button (VS Code automatically adds this) + vscodeWrapperStub.showWarningMessageAdvanced.resolves(undefined); + + const response = await confirmHandler!(undefined); + + expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( + LocConstants.DataTierApplication.DeployToExistingMessage, + { modal: true }, + [LocConstants.DataTierApplication.DeployToExistingConfirm], + ); + expect(response.confirmed).to.be.false; + }); + + test("confirmation dialog returns confirmed=false when user dismisses dialog (ESC)", async () => { + createController(); + + const confirmHandler = requestHandlers.get( + ConfirmDeployToExistingWebviewRequest.type.method, + ); + expect(confirmHandler, "Confirm handler was not registered").to.be.a("function"); + + // Mock user dismissing dialog with ESC (returns undefined) + vscodeWrapperStub.showWarningMessageAdvanced.resolves(undefined); + + const response = await confirmHandler!(undefined); + + expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( + LocConstants.DataTierApplication.DeployToExistingMessage, + { modal: true }, + [LocConstants.DataTierApplication.DeployToExistingConfirm], + ); + expect(response.confirmed).to.be.false; + }); + }); + + suite("Controller Initialization", () => { + test("creates webview panel with correct configuration", () => { + createController(); + + expect(createWebviewPanelStub).to.have.been.calledOnce; + expect(createWebviewPanelStub).to.have.been.calledWith( + "mssql-react-webview", + LocConstants.DataTierApplication.Title, + sinon.match.any, + sinon.match.any, + ); + }); + + test("registers all request handlers", () => { + createController(); + + expect(requestHandlers.has(DeployDacpacWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ExtractDacpacWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ImportBacpacWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ExportBacpacWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ValidateFilePathWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ListDatabasesWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ValidateDatabaseNameWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ListConnectionsWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ConnectToServerWebviewRequest.type.method)).to.be.true; + expect(requestHandlers.has(ConfirmDeployToExistingWebviewRequest.type.method)).to.be + .true; + }); + + test("registers cancel notification handler", () => { + createController(); + + expect( + notificationHandlers.has(CancelDataTierApplicationWebviewNotification.type.method), + ).to.be.true; + }); + + test("returns correct owner URI", () => { + createController(); + + expect(controller.ownerUri).to.equal(ownerUri); + }); + }); + + suite("Connection Operations", () => { + let connectionStoreStub: sinon.SinonStubbedInstance; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockConnections: any[]; + + setup(() => { + connectionStoreStub = sandbox.createStubInstance(ConnectionStore); + sandbox.stub(connectionManagerStub, "connectionStore").get(() => connectionStoreStub); + + // Create mock connection profiles + mockConnections = [ + { + server: "server1.database.windows.net", + database: "db1", + user: "admin", + profileName: "Server 1 - db1", + id: "conn1", + authenticationType: 2, // SQL Login + }, + { + server: "localhost", + database: "master", + user: undefined, + profileName: "Local Server", + id: "conn2", + authenticationType: 1, // Integrated + }, + { + server: "server2.database.windows.net", + database: undefined, + user: "user@domain.com", + profileName: "Azure Server", + id: "conn3", + authenticationType: 3, // Azure MFA + }, + ]; + }); + + test("lists connections successfully", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + + // Mock active connections - conn1 is connected + const mockActiveConnections = { + uri1: { + credentials: { + server: "server1.database.windows.net", + database: "db1", + }, + }, + }; + sandbox + .stub(connectionManagerStub, "activeConnections") + .get(() => mockActiveConnections); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({}); + + expect(result).to.exist; + expect(result.connections).to.have.lengthOf(3); + + // Verify first connection + const conn1 = result.connections[0]; + expect(conn1.server).to.equal("server1.database.windows.net"); + expect(conn1.database).to.equal("db1"); + expect(conn1.userName).to.equal("admin"); + expect(conn1.authenticationType).to.equal("SQL Login"); + expect(conn1.isConnected).to.be.true; + expect(conn1.profileId).to.equal("conn1"); + expect(conn1.displayName).to.include("Server 1 - db1"); + + // Verify second connection + const conn2 = result.connections[1]; + expect(conn2.server).to.equal("localhost"); + expect(conn2.authenticationType).to.equal("Integrated"); + expect(conn2.isConnected).to.be.false; + + // Verify third connection + const conn3 = result.connections[2]; + expect(conn3.server).to.equal("server2.database.windows.net"); + expect(conn3.authenticationType).to.equal("Azure MFA"); + expect(conn3.isConnected).to.be.false; + }); + + test("returns empty array when getRecentlyUsedConnections fails", async () => { + connectionStoreStub.getRecentlyUsedConnections.throws( + new Error("Connection store error"), + ); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({}); + + expect(result).to.exist; + expect(result.connections).to.be.an("array").that.is.empty; + }); + + test("builds display name correctly with all fields", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + const conn = result.connections[0]; + expect(conn.displayName).to.include("Server 1 - db1"); + expect(conn.displayName).to.include("(db1)"); + expect(conn.displayName).to.include("admin"); + }); + + test("builds display name without optional fields", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const minimalConnection: any = { + server: "testserver", + database: undefined, + user: undefined, + profileName: undefined, + id: "conn-minimal", + authenticationType: 1, + }; + + connectionStoreStub.getRecentlyUsedConnections.returns([minimalConnection]); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + const conn = result.connections[0]; + expect(conn.displayName).to.equal("testserver"); + }); + + test("connects to server successfully when not already connected", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionManagerStub.getUriForConnection.returns("new-owner-uri"); + connectionManagerStub.connect.resolves(true); + + // No active connections initially + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({ profileId: "conn1" }); + + expect(result).to.exist; + expect(result.isConnected).to.be.true; + expect(result.ownerUri).to.equal("new-owner-uri"); + expect(result.errorMessage).to.be.undefined; + + // Called twice: once to check if connected, once after connecting to get the URI + expect(connectionManagerStub.getUriForConnection).to.have.been.calledTwice; + expect(connectionManagerStub.connect).to.have.been.calledOnce; + }); + + test("retrieves ownerUri after successful connection when initially undefined", async () => { + // This test validates the bug fix where getUriForConnection returns undefined + // before connection (since connection doesn't exist yet), but after connect() + // succeeds, we call getUriForConnection again to get the actual URI + connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + + // First call returns undefined (connection doesn't exist yet) + // Second call returns the actual URI (after connection is established) + connectionManagerStub.getUriForConnection + .onFirstCall() + .returns(undefined) + .onSecondCall() + .returns("generated-owner-uri-123"); + + connectionManagerStub.connect.resolves(true); + + // No active connections initially + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({ profileId: "conn1" }); + + expect(result).to.exist; + expect(result.isConnected).to.be.true; + expect(result.ownerUri).to.equal("generated-owner-uri-123"); + expect(result.errorMessage).to.be.undefined; + + // Verify the sequence of calls + expect(connectionManagerStub.getUriForConnection).to.have.been.calledTwice; + expect(connectionManagerStub.connect).to.have.been.calledOnce; + // connect() should be called with empty string to let it generate the URI + expect(connectionManagerStub.connect).to.have.been.calledWith("", mockConnections[0]); + }); + + test("returns existing ownerUri when already connected", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionManagerStub.getUriForConnection.returns("existing-owner-uri"); + + // Mock that connection already exists + const mockActiveConnections = { + "existing-owner-uri": { + credentials: { + server: "server1.database.windows.net", + database: "db1", + }, + }, + }; + sandbox + .stub(connectionManagerStub, "activeConnections") + .get(() => mockActiveConnections); + + createController(); + + const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); + const result = await handler!({ profileId: "conn1" }); + + expect(result.isConnected).to.be.true; + expect(result.ownerUri).to.equal("existing-owner-uri"); + expect(result.errorMessage).to.be.undefined; + + // Should not call connect since already connected + expect(connectionManagerStub.connect).to.not.have.been.called; + }); + + test("returns error when profile not found", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); + const result = await handler!({ profileId: "non-existent-id" }); + + expect(result.isConnected).to.be.false; + expect(result.ownerUri).to.equal(""); + expect(result.errorMessage).to.equal("Connection profile not found"); + }); + + test("returns error when connection fails", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionManagerStub.getUriForConnection.returns("new-owner-uri"); + connectionManagerStub.connect.resolves(false); // Connection failed + + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); + const result = await handler!({ profileId: "conn1" }); + + expect(result.isConnected).to.be.false; + expect(result.ownerUri).to.equal(""); + expect(result.errorMessage).to.equal("Failed to connect to server"); + }); + + test("handles connection exception gracefully", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionManagerStub.getUriForConnection.returns("new-owner-uri"); + connectionManagerStub.connect.rejects(new Error("Network timeout")); + + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); + const result = await handler!({ profileId: "conn1" }); + + expect(result.isConnected).to.be.false; + expect(result.ownerUri).to.equal(""); + expect(result.errorMessage).to.include("Connection failed"); + expect(result.errorMessage).to.include("Network timeout"); + }); + + test("identifies connected server by matching server and database", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + + // Mock active connection with matching server and database + const mockActiveConnections = { + uri1: { + credentials: { + server: "localhost", + database: "master", + }, + }, + }; + sandbox + .stub(connectionManagerStub, "activeConnections") + .get(() => mockActiveConnections); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + // Find localhost connection + const localhostConn = result.connections.find((c) => c.server === "localhost"); + expect(localhostConn).to.exist; + expect(localhostConn!.isConnected).to.be.true; + }); + + test("identifies connected server when database is undefined in both", async () => { + const connectionWithoutDb = { + ...mockConnections[2], + database: undefined, + }; + connectionStoreStub.getRecentlyUsedConnections.returns([connectionWithoutDb]); + + // Mock active connection without database + const mockActiveConnections = { + uri1: { + credentials: { + server: "server2.database.windows.net", + database: undefined, + }, + }, + }; + sandbox + .stub(connectionManagerStub, "activeConnections") + .get(() => mockActiveConnections); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + expect(result.connections[0].isConnected).to.be.true; + }); + + test("generates profileId from server and database when id is missing", async () => { + const connectionWithoutId: (typeof mockConnections)[0] = { + ...mockConnections[0], + id: undefined, + }; + connectionStoreStub.getRecentlyUsedConnections.returns([connectionWithoutId]); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + expect(result.connections[0].profileId).to.equal("server1.database.windows.net_db1"); + }); + + test("matches connection by server and database when both provided", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + // Find the connection that matches server1.database.windows.net and db1 + const matchingConnection = result.connections.find( + (conn) => conn.server === "server1.database.windows.net" && conn.database === "db1", + ); + + expect(matchingConnection).to.exist; + expect(matchingConnection!.profileId).to.equal("conn1"); + }); + + test("matches connection by server only when database is not specified", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + // Find the connection that matches localhost (conn2 has master database) + const matchingConnection = result.connections.find( + (conn) => conn.server === "localhost" && conn.database === "master", + ); + + expect(matchingConnection).to.exist; + expect(matchingConnection!.profileId).to.equal("conn2"); + }); + + test("finds connection when database is undefined in profile", async () => { + // This tests the scenario where a server-level connection exists + // (database is undefined in the connection profile) + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + // conn3 has undefined database - should still be findable by server + const matchingConnection = result.connections.find( + (conn) => conn.server === "server2.database.windows.net", + ); + + expect(matchingConnection).to.exist; + expect(matchingConnection!.profileId).to.equal("conn3"); + expect(matchingConnection!.database).to.be.undefined; + }); + + test("connection matching is case-sensitive for server names", async () => { + connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); + const result = await handler!({}); + + // Case must match exactly + const matchingConnection = result.connections.find( + (conn) => conn.server === "LOCALHOST", // Different case + ); + + expect(matchingConnection).to.be.undefined; + + // Correct case should work + const correctMatch = result.connections.find((conn) => conn.server === "localhost"); + expect(correctMatch).to.exist; + }); + }); + + suite("Database Operations with Empty OwnerUri", () => { + test("returns empty array when ownerUri is empty for list databases", async () => { + createController(); + + const handler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({ ownerUri: "" }); + + expect(result).to.exist; + expect(result.databases).to.be.an("array").that.is.empty; + }); + + test("returns empty array when ownerUri is whitespace for list databases", async () => { + createController(); + + const handler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + const result = await handler!({ ownerUri: " " }); + + expect(result.databases).to.be.an("array").that.is.empty; + }); + + test("returns validation error when ownerUri is empty for database name validation", async () => { + createController(); + + const handler = requestHandlers.get(ValidateDatabaseNameWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({ + databaseName: "TestDB", + ownerUri: "", + shouldNotExist: true, + }); + + expect(result).to.exist; + expect(result.isValid).to.be.false; + expect(result.errorMessage).to.include("No active connection"); + }); + + test("returns validation error when ownerUri is whitespace for database name validation", async () => { + createController(); + + const handler = requestHandlers.get(ValidateDatabaseNameWebviewRequest.type.method); + const result = await handler!({ + databaseName: "TestDB", + ownerUri: " ", + shouldNotExist: true, + }); + + expect(result.isValid).to.be.false; + expect(result.errorMessage).to.include("No active connection"); + }); + }); +}); From 9b9e9829a239afc5eba8a2766753b7be727a9b14 Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 20 Oct 2025 15:50:39 -0600 Subject: [PATCH 02/79] small fix to enable button after success export --- localization/l10n/bundle.l10n.json | 62 +++++- localization/xliff/vscode-mssql.xlf | 183 ++++++++++++++++++ .../dataTierApplicationForm.tsx | 3 +- 3 files changed, 244 insertions(+), 4 deletions(-) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index aa10bad1e5..52accd8165 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -725,6 +725,57 @@ "Show Confirm Password": "Show Confirm Password", "Hide Confirm Password": "Hide Confirm Password", "Passwords do not match": "Passwords do not match", + "Data-tier Application": "Data-tier Application", + "Deploy, extract, import, or export data-tier applications on the selected database": "Deploy, extract, import, or export data-tier applications on the selected database", + "Operation": "Operation", + "Select an operation": "Select an operation", + "Select a server": "Select a server", + "No connections available. Please create a connection first.": "No connections available. Please create a connection first.", + "Connecting to server...": "Connecting to server...", + "Failed to connect to server": "Failed to connect to server", + "Deploy DACPAC": "Deploy DACPAC", + "Extract DACPAC": "Extract DACPAC", + "Import BACPAC": "Import BACPAC", + "Export BACPAC": "Export BACPAC", + "Deploy a DACPAC to create or update a database": "Deploy a DACPAC to create or update a database", + "Extract a DACPAC from an existing database": "Extract a DACPAC from an existing database", + "Import a BACPAC to create a new database": "Import a BACPAC to create a new database", + "Export a BACPAC from an existing database": "Export a BACPAC from an existing database", + "Package file": "Package file", + "Output file": "Output file", + "Select a DACPAC or BACPAC file": "Select a DACPAC or BACPAC file", + "Enter the path for the output file": "Enter the path for the output file", + "Browse...": "Browse...", + "Target Database": "Target Database", + "Source Database": "Source Database", + "Database Name": "Database Name", + "New Database": "New Database", + "Existing Database": "Existing Database", + "Select a database": "Select a database", + "Enter database name": "Enter database name", + "Application Name": "Application Name", + "Enter application name": "Enter application name", + "Application Version": "Application Version", + "Execute": "Execute", + "File path is required": "File path is required", + "Invalid file": "Invalid file", + "Database name is required": "Database name is required", + "Invalid database": "Invalid database", + "Validation failed": "Validation failed", + "Deploying DACPAC...": "Deploying DACPAC...", + "Extracting DACPAC...": "Extracting DACPAC...", + "Importing BACPAC...": "Importing BACPAC...", + "Exporting BACPAC...": "Exporting BACPAC...", + "Operation failed": "Operation failed", + "An unexpected error occurred": "An unexpected error occurred", + "DACPAC deployed successfully": "DACPAC deployed successfully", + "DACPAC extracted successfully": "DACPAC extracted successfully", + "BACPAC imported successfully": "BACPAC imported successfully", + "BACPAC exported successfully": "BACPAC exported successfully", + "Deploy to Existing Database": "Deploy to Existing Database", + "You are about to deploy to an existing database. This operation will make permanent changes to the database schema and may result in data loss. Do you want to continue?": "You are about to deploy to an existing database. This operation will make permanent changes to the database schema and may result in data loss. Do you want to continue?", + "Deploy": "Deploy", + "A database with this name already exists on the server": "A database with this name already exists on the server", "Object Explorer Filter": "Object Explorer Filter", "Azure MFA": "Azure MFA", "Windows Authentication": "Windows Authentication", @@ -1107,7 +1158,6 @@ }, "Insert": "Insert", "Update": "Update", - "Execute": "Execute", "Alter": "Alter", "Signing in to Azure...": "Signing in to Azure...", "Are you sure you want to delete {0}? You can delete its connections as well, or move them to the root folder./{0} is the group name": { @@ -1252,7 +1302,6 @@ "comment": ["{0} is the number of invalid accounts that have been removed"] }, "Entra token cache cleared successfully.": "Entra token cache cleared successfully.", - "Database Name": "Database Name", "Enter Database Name": "Enter Database Name", "Database Name is required": "Database Name is required", "Database Description": "Database Description", @@ -1444,7 +1493,6 @@ "Publish Project": "Publish Project", "Publish Profile": "Publish Profile", "Select or enter a publish profile": "Select or enter a publish profile", - "Database name is required": "Database name is required", "SQLCMD Variables": "SQLCMD Variables", "Publish Target": "Publish Target", "Existing SQL server": "Existing SQL server", @@ -1772,6 +1820,14 @@ "message": "Edit Connection Group - {0}", "comment": ["{0} is the connection group name"] }, + "File not found": "File not found", + "Invalid file extension. Expected .dacpac or .bacpac": "Invalid file extension. Expected .dacpac or .bacpac", + "Directory not found": "Directory not found", + "File already exists. It will be overwritten if you continue": "File already exists. It will be overwritten if you continue", + "Database name contains invalid characters. Avoid using: < > * ? \" / \\ |": "Database name contains invalid characters. Avoid using: < > * ? \" / \\ |", + "Database name is too long. Maximum length is 128 characters": "Database name is too long. Maximum length is 128 characters", + "Database not found on the server": "Database not found on the server", + "Validation failed. Please check your inputs": "Validation failed. Please check your inputs", "Azure sign in failed.": "Azure sign in failed.", "Select subscriptions": "Select subscriptions", "Error loading Azure subscriptions.": "Error loading Azure subscriptions.", diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 4b7bcb4146..04bc07a56c 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -35,6 +35,9 @@ A SQL editor must have focus before executing this command + + A database with this name already exists on the server + A firewall rule is required to access this server. @@ -185,6 +188,9 @@ An error occurred: {0} {0} is the error message + + An unexpected error occurred + An unexpected error occurred with the language model. Please try again. @@ -200,6 +206,12 @@ Application Intent + + Application Name + + + Application Version + Apply @@ -320,6 +332,12 @@ Azure: Sign In with Device Code + + BACPAC exported successfully + + + BACPAC imported successfully + Back @@ -351,6 +369,9 @@ Browse Fabric + + Browse... + CSV @@ -734,6 +755,9 @@ {0} is the server name {1} is the database name + + Connecting to server... + Connecting to your SQL Server Docker container @@ -941,6 +965,12 @@ Custom Zoom + + DACPAC deployed successfully + + + DACPAC extracted successfully + Data Type @@ -954,6 +984,9 @@ {2} is target data type {3} is target column + + Data-tier Application + Data-tier Application File (.dacpac) @@ -985,9 +1018,18 @@ Database name + + Database name contains invalid characters. Avoid using: < > * ? " / \ | + Database name is required + + Database name is too long. Maximum length is 128 characters + + + Database not found on the server + Default @@ -1021,6 +1063,24 @@ Deny + + Deploy + + + Deploy DACPAC + + + Deploy a DACPAC to create or update a database + + + Deploy to Existing Database + + + Deploy, extract, import, or export data-tier applications on the selected database + + + Deploying DACPAC... + Deployment Failed @@ -1042,6 +1102,9 @@ Developer-friendly transactional database using the Azure SQL Database Engine. + + Directory not found + Disable intellisense and syntax error checking on current document @@ -1162,12 +1225,18 @@ Enter Database Name + + Enter application name + Enter connection group name Enter container name + + Enter database name + Enter description (optional) @@ -1192,6 +1261,9 @@ Enter profile name + + Enter the path for the output file + Entra token cache cleared successfully. @@ -1316,6 +1388,9 @@ Execution Plan + + Existing Database + Existing SQL server @@ -1337,9 +1412,27 @@ Export + + Export BACPAC + + + Export a BACPAC from an existing database + + + Exporting BACPAC... + Expression + + Extract DACPAC + + + Extract a DACPAC from an existing database + + + Extracting DACPAC... + Extremely likely @@ -1388,6 +1481,9 @@ Failed to connect to database: {0} {0} is the database name + + Failed to connect to server + Failed to connect to server. @@ -1481,6 +1577,15 @@ File + + File already exists. It will be overwritten if you continue + + + File not found + + + File path is required + Filter @@ -1698,9 +1803,18 @@ Ignore Tenant + + Import BACPAC + + + Import a BACPAC to create a new database + Importance + + Importing BACPAC... + In progress @@ -1761,6 +1875,15 @@ Invalid connection string: {0} + + Invalid database + + + Invalid file + + + Invalid file extension. Expected .dacpac or .bacpac + Invalid max length '{0}' {0} is the max length @@ -2097,6 +2220,9 @@ New Column Mapping + + New Database + New Deployment @@ -2173,6 +2299,9 @@ No connection was found. Please connect to a server first. + + No connections available. Please create a connection first. + No database objects found matching '{0}' {0} is the search term @@ -2325,6 +2454,12 @@ Opening schema designer... + + Operation + + + Operation failed + Operator @@ -2340,6 +2475,9 @@ Options have changed. Recompare to see the comparison? + + Output file + Overall, how satisfied are you with the MSSQL extension? @@ -2350,6 +2488,9 @@ PNG + + Package file + Parameters @@ -2818,12 +2959,21 @@ Select Target Schema + + Select a DACPAC or BACPAC file + Select a Workspace Select a connection group + + Select a database + + + Select a server + Select a tenant @@ -2854,6 +3004,9 @@ Select an object to view its definition ({0} results) {0} is the number of results + + Select an operation + Select image @@ -3012,6 +3165,9 @@ Source Column + + Source Database + Source Name @@ -3144,6 +3300,9 @@ Target + + Target Database + Target Name @@ -3364,6 +3523,12 @@ Using {0} to process your request... {0} is the model name that will be processing the request + + Validation failed + + + Validation failed. Please check your inputs + Value @@ -3412,6 +3577,9 @@ Yes + + You are about to deploy to an existing database. This operation will make permanent changes to the database schema and may result in data loss. Do you want to continue? + You are not connected to any database. @@ -3728,6 +3896,9 @@ Create a new table in your database, or edit existing tables with the table designer. Once you're done making your changes, click the 'Publish' button to send the changes to your database. + + Data-tier Application + Default view mode for query results display. @@ -3740,6 +3911,9 @@ Delete Container + + Deploy DACPAC + Disable Actual Plan @@ -3887,6 +4061,12 @@ Explain Query (Preview) + + Export BACPAC + + + Extract DACPAC + Familiarize yourself with more features of the MSSQL extension that can help you be more productive. @@ -3905,6 +4085,9 @@ Give Feedback + + Import BACPAC + MSSQL Copilot (Preview) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index b93d2c8ee1..90aedb5964 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -515,11 +515,13 @@ export const DataTierApplicationForm = () => { if (result?.success) { setSuccessMessage(getSuccessMessage(operationType)); setProgressMessage(""); + setIsOperationInProgress(false); } else { setErrorMessage( result?.errorMessage || locConstants.dataTierApplication.operationFailed, ); setProgressMessage(""); + setIsOperationInProgress(false); } } catch (error) { setErrorMessage( @@ -528,7 +530,6 @@ export const DataTierApplicationForm = () => { : locConstants.dataTierApplication.unexpectedError, ); setProgressMessage(""); - } finally { setIsOperationInProgress(false); } }; From 9d48496ba18b1dd580d55cb789f4682714f8a0a6 Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 20 Oct 2025 16:22:52 -0600 Subject: [PATCH 03/79] fix on select database by default --- src/controllers/mainController.ts | 10 +++++----- .../DataTierApplication/dataTierApplicationForm.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 7b809a8fd5..43e73cb95b 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -1779,7 +1779,7 @@ export default class MainController implements vscode.Disposable { ? this._connectionMgr.getUriForConnection(connectionProfile) : ""; const serverName = connectionProfile?.server || ""; - const databaseName = connectionProfile?.database || ""; + const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; const initialState: DataTierApplicationWebviewState = { ownerUri, @@ -1811,7 +1811,7 @@ export default class MainController implements vscode.Disposable { ? this._connectionMgr.getUriForConnection(connectionProfile) : ""; const serverName = connectionProfile?.server || ""; - const databaseName = connectionProfile?.database || ""; + const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; const initialState: DataTierApplicationWebviewState = { ownerUri, @@ -1843,7 +1843,7 @@ export default class MainController implements vscode.Disposable { ? this._connectionMgr.getUriForConnection(connectionProfile) : ""; const serverName = connectionProfile?.server || ""; - const databaseName = connectionProfile?.database || ""; + const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; const initialState: DataTierApplicationWebviewState = { ownerUri, @@ -1875,7 +1875,7 @@ export default class MainController implements vscode.Disposable { ? this._connectionMgr.getUriForConnection(connectionProfile) : ""; const serverName = connectionProfile?.server || ""; - const databaseName = connectionProfile?.database || ""; + const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; const initialState: DataTierApplicationWebviewState = { ownerUri, @@ -1907,7 +1907,7 @@ export default class MainController implements vscode.Disposable { ? this._connectionMgr.getUriForConnection(connectionProfile) : ""; const serverName = connectionProfile?.server || ""; - const databaseName = connectionProfile?.database || ""; + const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; const initialState: DataTierApplicationWebviewState = { ownerUri, diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 90aedb5964..63f9c0252a 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -122,8 +122,10 @@ export const DataTierApplicationForm = () => { ); const [filePath, setFilePath] = useState(""); const [databaseName, setDatabaseName] = useState(initialDatabaseName || ""); - const [isNewDatabase, setIsNewDatabase] = useState(true); - const [availableDatabases, setAvailableDatabases] = useState([]); + const [isNewDatabase, setIsNewDatabase] = useState(!initialDatabaseName); + const [availableDatabases, setAvailableDatabases] = useState( + initialDatabaseName ? [initialDatabaseName] : [], + ); const [applicationName, setApplicationName] = useState(""); const [applicationVersion, setApplicationVersion] = useState("1.0.0"); const [isOperationInProgress, setIsOperationInProgress] = useState(false); From e79fedc9ab649e7db4a57325bc9faba54133903a Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 20 Oct 2025 17:10:54 -0600 Subject: [PATCH 04/79] fix on connecting when lunching from database --- .../dataTierApplicationForm.tsx | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 63f9c0252a..a5824aa5ce 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -164,9 +164,25 @@ export const DataTierApplicationForm = () => { if (result?.connections) { setAvailableConnections(result.connections); - // If we have initial server/database from Object Explorer, find and select the matching connection - if (initialServerName && result.connections.length > 0) { - // Match by server and database (or server only if database is not specified) + // If we have initial ownerUri from Object Explorer, we're already connected + // Just find and select the matching connection without trying to connect + if (initialOwnerUri && initialServerName && result.connections.length > 0) { + const matchingConnection = result.connections.find((conn) => { + const serverMatches = conn.server === initialServerName; + const databaseMatches = + !initialDatabaseName || + !conn.database || + conn.database === initialDatabaseName; + return serverMatches && databaseMatches; + }); + + if (matchingConnection) { + setSelectedProfileId(matchingConnection.profileId); + // We already have ownerUri from Object Explorer, no need to connect + setOwnerUri(initialOwnerUri); + } + } else if (initialServerName && result.connections.length > 0) { + // No ownerUri yet, need to find and potentially connect const matchingConnection = result.connections.find((conn) => { const serverMatches = conn.server === initialServerName; const databaseMatches = @@ -214,9 +230,18 @@ export const DataTierApplicationForm = () => { setIsConnecting(false); } } else { - // Already connected, just set the ownerUri - if (initialOwnerUri) { - setOwnerUri(initialOwnerUri); + // Already connected, get the ownerUri from the connect request + try { + const connectResult = await context?.extensionRpc?.sendRequest( + ConnectToServerWebviewRequest.type, + { profileId: matchingConnection.profileId }, + ); + + if (connectResult?.ownerUri) { + setOwnerUri(connectResult.ownerUri); + } + } catch (error) { + console.error("Failed to get ownerUri:", error); } } } From f2b99dd243175f243a46c5d042814a4102b03752 Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 08:30:32 -0600 Subject: [PATCH 05/79] small fixes in ui --- .../dataTierApplicationWebviewController.ts | 29 +++- src/controllers/mainController.ts | 25 +++ .../dataTierApplicationForm.tsx | 164 +++++++++--------- ...taTierApplicationWebviewController.test.ts | 2 +- 4 files changed, 140 insertions(+), 80 deletions(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index 17b9d02cb3..c5a8360205 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -446,7 +446,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr // Get active connections const activeConnections = this.connectionManager.activeConnections; - // Build the connection profile list + // Build the connection profile list from recent connections for (const conn of recentConnections) { const profile = conn as IConnectionProfile; const displayName = this.buildConnectionDisplayName(profile); @@ -473,6 +473,33 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }); } + const existingProfileIds = new Set(connections.map((conn) => conn.profileId)); + + // Include active connections that may not appear in the recent list + for (const activeConnection of Object.values(activeConnections)) { + const profile = activeConnection.credentials as IConnectionProfile; + const profileId = profile.id || `${profile.server}_${profile.database || ""}`; + + if (existingProfileIds.has(profileId)) { + continue; + } + + const displayName = this.buildConnectionDisplayName(profile); + + connections.push({ + displayName, + server: profile.server, + database: profile.database, + authenticationType: this.getAuthenticationTypeString( + profile.authenticationType, + ), + userName: profile.user, + isConnected: true, + profileId, + }); + existingProfileIds.add(profileId); + } + return { connections }; } catch (error) { this.logger.error(`Failed to list connections: ${error}`); diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 43e73cb95b..4403cf39a9 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -1780,11 +1780,16 @@ export default class MainController implements vscode.Disposable { : ""; const serverName = connectionProfile?.server || ""; const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; + const profileId = connectionProfile + ? connectionProfile.id || + `${connectionProfile.server}_${connectionProfile.database || ""}` + : undefined; const initialState: DataTierApplicationWebviewState = { ownerUri, serverName, databaseName, + selectedProfileId: profileId, operationType: undefined, }; @@ -1812,11 +1817,16 @@ export default class MainController implements vscode.Disposable { : ""; const serverName = connectionProfile?.server || ""; const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; + const profileId = connectionProfile + ? connectionProfile.id || + `${connectionProfile.server}_${connectionProfile.database || ""}` + : undefined; const initialState: DataTierApplicationWebviewState = { ownerUri, serverName, databaseName, + selectedProfileId: profileId, operationType: DataTierOperationType.Deploy, }; @@ -1844,11 +1854,16 @@ export default class MainController implements vscode.Disposable { : ""; const serverName = connectionProfile?.server || ""; const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; + const profileId = connectionProfile + ? connectionProfile.id || + `${connectionProfile.server}_${connectionProfile.database || ""}` + : undefined; const initialState: DataTierApplicationWebviewState = { ownerUri, serverName, databaseName, + selectedProfileId: profileId, operationType: DataTierOperationType.Extract, }; @@ -1876,11 +1891,16 @@ export default class MainController implements vscode.Disposable { : ""; const serverName = connectionProfile?.server || ""; const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; + const profileId = connectionProfile + ? connectionProfile.id || + `${connectionProfile.server}_${connectionProfile.database || ""}` + : undefined; const initialState: DataTierApplicationWebviewState = { ownerUri, serverName, databaseName, + selectedProfileId: profileId, operationType: DataTierOperationType.Import, }; @@ -1908,11 +1928,16 @@ export default class MainController implements vscode.Disposable { : ""; const serverName = connectionProfile?.server || ""; const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; + const profileId = connectionProfile + ? connectionProfile.id || + `${connectionProfile.server}_${connectionProfile.database || ""}` + : undefined; const initialState: DataTierApplicationWebviewState = { ownerUri, serverName, databaseName, + selectedProfileId: profileId, operationType: DataTierOperationType.Export, }; diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index a5824aa5ce..889621abe4 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -115,6 +115,9 @@ export const DataTierApplicationForm = () => { const initialOwnerUri = useDataTierApplicationSelector((state) => state.ownerUri); const initialServerName = useDataTierApplicationSelector((state) => state.serverName); const initialDatabaseName = useDataTierApplicationSelector((state) => state.databaseName); + const initialSelectedProfileId = useDataTierApplicationSelector( + (state) => state.selectedProfileId, + ); // Local state const [operationType, setOperationType] = useState( @@ -134,7 +137,9 @@ export const DataTierApplicationForm = () => { const [successMessage, setSuccessMessage] = useState(""); const [validationErrors, setValidationErrors] = useState>({}); const [availableConnections, setAvailableConnections] = useState([]); - const [selectedProfileId, setSelectedProfileId] = useState(""); + const [selectedProfileId, setSelectedProfileId] = useState( + initialSelectedProfileId || "", + ); const [ownerUri, setOwnerUri] = useState(initialOwnerUri || ""); const [isConnecting, setIsConnecting] = useState(false); @@ -164,85 +169,91 @@ export const DataTierApplicationForm = () => { if (result?.connections) { setAvailableConnections(result.connections); - // If we have initial ownerUri from Object Explorer, we're already connected - // Just find and select the matching connection without trying to connect - if (initialOwnerUri && initialServerName && result.connections.length > 0) { - const matchingConnection = result.connections.find((conn) => { - const serverMatches = conn.server === initialServerName; - const databaseMatches = - !initialDatabaseName || - !conn.database || - conn.database === initialDatabaseName; - return serverMatches && databaseMatches; - }); - - if (matchingConnection) { - setSelectedProfileId(matchingConnection.profileId); - // We already have ownerUri from Object Explorer, no need to connect - setOwnerUri(initialOwnerUri); + const findMatchingConnection = (): ConnectionProfile | undefined => { + if (initialSelectedProfileId) { + const byProfileId = result.connections.find( + (conn) => conn.profileId === initialSelectedProfileId, + ); + if (byProfileId) { + return byProfileId; + } } - } else if (initialServerName && result.connections.length > 0) { - // No ownerUri yet, need to find and potentially connect - const matchingConnection = result.connections.find((conn) => { - const serverMatches = conn.server === initialServerName; - const databaseMatches = - !initialDatabaseName || - !conn.database || - conn.database === initialDatabaseName; - return serverMatches && databaseMatches; - }); - - if (matchingConnection) { - setSelectedProfileId(matchingConnection.profileId); - - // Auto-connect if not already connected + + if (initialServerName) { + return result.connections.find((conn) => { + const serverMatches = conn.server === initialServerName; + const databaseMatches = + !initialDatabaseName || + !conn.database || + conn.database === initialDatabaseName; + return serverMatches && databaseMatches; + }); + } + + return undefined; + }; + + const matchingConnection = findMatchingConnection(); + + if (matchingConnection) { + setSelectedProfileId(matchingConnection.profileId); + + if (initialOwnerUri) { + // Already connected via Object Explorer + setOwnerUri(initialOwnerUri); if (!matchingConnection.isConnected) { - setIsConnecting(true); - try { - const connectResult = await context?.extensionRpc?.sendRequest( - ConnectToServerWebviewRequest.type, - { profileId: matchingConnection.profileId }, + setAvailableConnections((prev) => + prev.map((conn) => + conn.profileId === matchingConnection.profileId + ? { ...conn, isConnected: true } + : conn, + ), + ); + } + } else if (!matchingConnection.isConnected) { + setIsConnecting(true); + try { + const connectResult = await context?.extensionRpc?.sendRequest( + ConnectToServerWebviewRequest.type, + { profileId: matchingConnection.profileId }, + ); + + if (connectResult?.isConnected && connectResult.ownerUri) { + setOwnerUri(connectResult.ownerUri); + setAvailableConnections((prev) => + prev.map((conn) => + conn.profileId === matchingConnection.profileId + ? { ...conn, isConnected: true } + : conn, + ), ); - - if (connectResult?.isConnected && connectResult.ownerUri) { - setOwnerUri(connectResult.ownerUri); - // Update the connection status in our list - setAvailableConnections((prev) => - prev.map((conn) => - conn.profileId === matchingConnection.profileId - ? { ...conn, isConnected: true } - : conn, - ), - ); - } else { - setErrorMessage( - connectResult?.errorMessage || - locConstants.dataTierApplication.connectionFailed, - ); - } - } catch (error) { - const errorMsg = - error instanceof Error ? error.message : String(error); + } else { setErrorMessage( - `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, + connectResult?.errorMessage || + locConstants.dataTierApplication.connectionFailed, ); - } finally { - setIsConnecting(false); } - } else { - // Already connected, get the ownerUri from the connect request - try { - const connectResult = await context?.extensionRpc?.sendRequest( - ConnectToServerWebviewRequest.type, - { profileId: matchingConnection.profileId }, - ); - - if (connectResult?.ownerUri) { - setOwnerUri(connectResult.ownerUri); - } - } catch (error) { - console.error("Failed to get ownerUri:", error); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + setErrorMessage( + `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, + ); + } finally { + setIsConnecting(false); + } + } else { + // Already connected, fetch ownerUri to ensure we have it + try { + const connectResult = await context?.extensionRpc?.sendRequest( + ConnectToServerWebviewRequest.type, + { profileId: matchingConnection.profileId }, + ); + + if (connectResult?.ownerUri) { + setOwnerUri(connectResult.ownerUri); } + } catch (error) { + console.error("Failed to get ownerUri:", error); } } } @@ -720,7 +731,7 @@ export const DataTierApplicationForm = () => { selectedProfileId ? availableConnections.find( (conn) => conn.profileId === selectedProfileId, - )?.displayName + )?.displayName || "" : "" } selectedOptions={selectedProfileId ? [selectedProfileId] : []} @@ -736,10 +747,7 @@ export const DataTierApplicationForm = () => { ) : ( availableConnections.map((conn) => ( - diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index 82034fea5d..2c00a25c3c 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -959,7 +959,7 @@ suite("DataTierApplicationWebviewController", () => { const result = await handler!({}); expect(result).to.exist; - expect(result.connections).to.have.lengthOf(3); + expect(result.connections).to.have.lengthOf(4); // Verify first connection const conn1 = result.connections[0]; From 68f0924d20c005bc9fe19610eeb277968d5a427d Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 10:03:58 -0600 Subject: [PATCH 06/79] making app and version optional --- .../dataTierApplicationForm.tsx | 18 ++++++------------ src/services/dacFxService.ts | 8 ++++---- src/sharedInterfaces/dataTierApplication.ts | 4 ++-- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 889621abe4..c354722650 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -640,11 +640,6 @@ export const DataTierApplicationForm = () => { const isFormValid = () => { if (!filePath || !databaseName) return false; - if ( - operationType === DataTierOperationType.Extract && - (!applicationName || !applicationVersion) - ) - return false; return Object.keys(validationErrors).length === 0; }; @@ -747,7 +742,10 @@ export const DataTierApplicationForm = () => { ) : ( availableConnections.map((conn) => ( - @@ -890,9 +888,7 @@ export const DataTierApplicationForm = () => { {showApplicationInfo && (
- + setApplicationName(data.value)} @@ -901,9 +897,7 @@ export const DataTierApplicationForm = () => { /> - + setApplicationVersion(data.value)} diff --git a/src/services/dacFxService.ts b/src/services/dacFxService.ts index fdf4779a26..f0d551c76a 100644 --- a/src/services/dacFxService.ts +++ b/src/services/dacFxService.ts @@ -44,16 +44,16 @@ export class DacFxService implements mssql.IDacFxService { public extractDacpac( databaseName: string, packageFilePath: string, - applicationName: string, - applicationVersion: string, + applicationName: string | undefined, + applicationVersion: string | undefined, ownerUri: string, taskExecutionMode: TaskExecutionMode, ): Thenable { const params: mssql.ExtractParams = { databaseName: databaseName, packageFilePath: packageFilePath, - applicationName: applicationName, - applicationVersion: applicationVersion, + applicationName: applicationName || databaseName, + applicationVersion: applicationVersion || "1.0.0.0", ownerUri: ownerUri, extractTarget: ExtractTarget.dacpac, taskExecutionMode: taskExecutionMode, diff --git a/src/sharedInterfaces/dataTierApplication.ts b/src/sharedInterfaces/dataTierApplication.ts index b449fb6057..e56c57deda 100644 --- a/src/sharedInterfaces/dataTierApplication.ts +++ b/src/sharedInterfaces/dataTierApplication.ts @@ -127,8 +127,8 @@ export interface DeployDacpacParams { export interface ExtractDacpacParams { databaseName: string; packageFilePath: string; - applicationName: string; - applicationVersion: string; + applicationName?: string; + applicationVersion?: string; ownerUri: string; } From 9ca59e4e308003b49fbd287a0479e0c35f90e7a9 Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 11:21:19 -0600 Subject: [PATCH 07/79] small text change --- localization/l10n/bundle.l10n.json | 2 +- localization/xliff/vscode-mssql.xlf | 2 +- src/reactviews/common/locConstants.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index 52accd8165..4d113084de 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -743,7 +743,7 @@ "Export a BACPAC from an existing database": "Export a BACPAC from an existing database", "Package file": "Package file", "Output file": "Output file", - "Select a DACPAC or BACPAC file": "Select a DACPAC or BACPAC file", + "Select package file": "Select package file", "Enter the path for the output file": "Enter the path for the output file", "Browse...": "Browse...", "Target Database": "Target Database", diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 04bc07a56c..dde3f1cf66 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -2960,7 +2960,7 @@ Select Target Schema - Select a DACPAC or BACPAC file + Select package file Select a Workspace diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index 20a5c870f3..8885237caf 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -1079,7 +1079,7 @@ export class LocConstants { exportDescription: l10n.t("Export a BACPAC from an existing database"), packageFileLabel: l10n.t("Package file"), outputFileLabel: l10n.t("Output file"), - selectPackageFile: l10n.t("Select a DACPAC or BACPAC file"), + selectPackageFile: l10n.t("Select package file"), selectOutputFile: l10n.t("Enter the path for the output file"), browse: l10n.t("Browse..."), targetDatabaseLabel: l10n.t("Target Database"), From d72676109a8976adc7bcf5e24a9745b7e2573c28 Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 11:24:16 -0600 Subject: [PATCH 08/79] loc changes --- localization/xliff/vscode-mssql.xlf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index dde3f1cf66..54cd9f4b55 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -2959,9 +2959,6 @@ Select Target Schema - - Select package file - Select a Workspace @@ -3017,6 +3014,9 @@ Select or enter a publish profile + + Select package file + Select profile to remove From ad3ce876ddbaad9614c5390bd7c51cb1aace2ac1 Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 12:29:15 -0600 Subject: [PATCH 09/79] Update src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../pages/DataTierApplication/dataTierApplicationForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index c354722650..4a50545c98 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -373,8 +373,8 @@ export const DataTierApplicationForm = () => { })); } else { setValidationErrors((prev) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { filePath: _fp, ...rest } = prev; + + const { filePath, ...rest } = prev; return rest; }); } From 03f5260a2c9fbe399c95025989a0e53c151a40ff Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 12:30:39 -0600 Subject: [PATCH 10/79] Update src/controllers/dataTierApplicationWebviewController.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/controllers/dataTierApplicationWebviewController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index c5a8360205..0d75fee26e 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -648,7 +648,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr // For Deploy operations, always warn if database exists to trigger confirmation // This ensures confirmation dialog is shown in both cases: // 1. User selected "New Database" but database already exists (shouldNotExist=true) - // 2. User selected "Existing Database" and selected existing database (shouldNotExist=true) + // 2. User selected "Existing Database" and selected existing database (shouldNotExist=false) if (operationType === DataTierOperationType.Deploy && exists) { return { isValid: true, // Allow the operation but with a warning From 3914a996e9e7f5bbbef51cce940a8c92f0ce8dc9 Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 12:58:13 -0600 Subject: [PATCH 11/79] fix on validation --- .../DataTierApplication/dataTierApplicationForm.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 4a50545c98..7ee9f27eb0 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -372,11 +372,10 @@ export const DataTierApplicationForm = () => { filePath: result.errorMessage || "", // This is a warning about overwrite })); } else { - setValidationErrors((prev) => { - - const { filePath, ...rest } = prev; - return rest; - }); + setValidationErrors((prev) => ({ + ...prev, + filePath: "", + })); } return true; } catch (error) { From ab7793809af474d714f4148e90d5f8e649c7716a Mon Sep 17 00:00:00 2001 From: allancascante Date: Tue, 21 Oct 2025 16:39:06 -0600 Subject: [PATCH 12/79] fixing test failing in pipeline --- .../dataTierApplicationWebviewController.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index 2c00a25c3c..c4a5ddb29d 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -42,6 +42,7 @@ import { ListDatabasesRequest } from "../../src/models/contracts/connection"; import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; import { ConnectionStore } from "../../src/models/connectionStore"; import * as fs from "fs"; +import * as path from "path"; chai.use(sinonChai); @@ -477,17 +478,23 @@ suite("DataTierApplicationWebviewController", () => { test("validates output file path when directory exists", async () => { // File doesn't exist, but directory does - fsExistsSyncStub.withArgs("C:\\output\\database.dacpac").returns(false); - fsExistsSyncStub.withArgs("C:\\output").returns(true); + const testDir = path.join( + path.sep === "\\" ? "C:\\database-test-folder" : "/database-test-folder", + ); + const testFile = path.join(testDir, "database.dacpac"); + + fsExistsSyncStub.withArgs(testFile).returns(false); + fsExistsSyncStub.withArgs(testDir).returns(true); createController(); const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ - filePath: "C:\\output\\database.dacpac", + filePath: testFile, shouldExist: false, }); expect(response.isValid).to.be.true; + expect(response.errorMessage).to.be.undefined; }); test("warns when output file already exists", async () => { From fa3e245e278101acab4dce94e4159a2e1912c9c4 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 22 Oct 2025 18:02:44 -0600 Subject: [PATCH 13/79] small improvement in validation --- .../dataTierApplicationWebviewController.ts | 7 +- .../dataTierApplicationForm.tsx | 149 +++++++++++++----- 2 files changed, 114 insertions(+), 42 deletions(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index 0d75fee26e..0ebe7e94db 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; import * as path from "path"; import * as fs from "fs"; +import { existsSync } from "fs"; import ConnectionManager from "./connectionManager"; import { DacFxService } from "../services/dacFxService"; import { IConnectionProfile } from "../models/interfaces"; @@ -375,9 +376,9 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }; } - const fileExists = fs.existsSync(filePath); + const fileFound = existsSync(filePath); - if (shouldExist && !fileExists) { + if (shouldExist && !fileFound) { return { isValid: false, errorMessage: LocConstants.DataTierApplication.FileNotFound, @@ -403,7 +404,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr } // Check if file already exists (for output files) - if (fileExists) { + if (fileFound) { // This is just a warning - the operation can continue with user confirmation return { isValid: true, diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 7ee9f27eb0..9b4809eeff 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -41,6 +41,14 @@ import { DataTierApplicationContext } from "./dataTierApplicationStateProvider"; import { useDataTierApplicationSelector } from "./dataTierApplicationSelector"; import { locConstants } from "../../common/locConstants"; +/** + * Validation message with severity level + */ +interface ValidationMessage { + message: string; + severity: "error" | "warning"; +} + const useStyles = makeStyles({ root: { display: "flex", @@ -135,7 +143,9 @@ export const DataTierApplicationForm = () => { const [progressMessage, setProgressMessage] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const [successMessage, setSuccessMessage] = useState(""); - const [validationErrors, setValidationErrors] = useState>({}); + const [validationMessages, setValidationMessages] = useState>( + {}, + ); const [availableConnections, setAvailableConnections] = useState([]); const [selectedProfileId, setSelectedProfileId] = useState( initialSelectedProfileId || "", @@ -267,7 +277,8 @@ export const DataTierApplicationForm = () => { setSelectedProfileId(profileId); setErrorMessage(""); setSuccessMessage(""); - setValidationErrors({}); + setValidationMessages({}); + setIsConnecting(true); // Find the selected connection const selectedConnection = availableConnections.find( @@ -344,9 +355,12 @@ export const DataTierApplicationForm = () => { const validateFilePath = async (path: string, shouldExist: boolean): Promise => { if (!path) { - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - filePath: locConstants.dataTierApplication.filePathRequired, + filePath: { + message: locConstants.dataTierApplication.filePathRequired, + severity: "error", + }, })); return false; } @@ -358,24 +372,32 @@ export const DataTierApplicationForm = () => { ); if (!result?.isValid) { - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - filePath: result?.errorMessage || locConstants.dataTierApplication.invalidFile, + filePath: { + message: + result?.errorMessage || locConstants.dataTierApplication.invalidFile, + severity: "error", + }, })); return false; } // Clear error or set warning for file overwrite if (result.errorMessage) { - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - filePath: result.errorMessage || "", // This is a warning about overwrite + filePath: { + message: result.errorMessage || "", + severity: "warning", // This is a warning about overwrite + }, })); } else { - setValidationErrors((prev) => ({ - ...prev, - filePath: "", - })); + setValidationMessages((prev) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { filePath: _fp, ...rest } = prev; + return rest; + }); } return true; } catch (error) { @@ -383,9 +405,12 @@ export const DataTierApplicationForm = () => { error instanceof Error ? error.message : locConstants.dataTierApplication.validationFailed; - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - filePath: errorMessage, + filePath: { + message: errorMessage, + severity: "error", + }, })); return false; } @@ -396,9 +421,12 @@ export const DataTierApplicationForm = () => { shouldNotExist: boolean, ): Promise => { if (!dbName) { - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - databaseName: locConstants.dataTierApplication.databaseNameRequired, + databaseName: { + message: locConstants.dataTierApplication.databaseNameRequired, + severity: "error", + }, })); return false; } @@ -415,16 +443,20 @@ export const DataTierApplicationForm = () => { ); if (!result?.isValid) { - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - databaseName: - result?.errorMessage || locConstants.dataTierApplication.invalidDatabase, + databaseName: { + message: + result?.errorMessage || + locConstants.dataTierApplication.invalidDatabase, + severity: "error", + }, })); return false; } // Clear validation errors if valid - setValidationErrors((prev) => { + setValidationMessages((prev) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { databaseName: _dn, ...rest } = prev; return rest; @@ -452,9 +484,12 @@ export const DataTierApplicationForm = () => { error instanceof Error ? error.message : locConstants.dataTierApplication.validationFailed; - setValidationErrors((prev) => ({ + setValidationMessages((prev) => ({ ...prev, - databaseName: errorMessage, + databaseName: { + message: errorMessage, + severity: "error", + }, })); return false; } @@ -597,9 +632,9 @@ export const DataTierApplicationForm = () => { if (result?.filePath) { setFilePath(result.filePath); // Clear validation error when file is selected - const newErrors = { ...validationErrors }; - delete newErrors.filePath; - setValidationErrors(newErrors); + const newMessages = { ...validationMessages }; + delete newMessages.filePath; + setValidationMessages(newMessages); // Validate the selected file path await validateFilePath(result.filePath, requiresInputFile); } @@ -639,7 +674,12 @@ export const DataTierApplicationForm = () => { const isFormValid = () => { if (!filePath || !databaseName) return false; - return Object.keys(validationErrors).length === 0; + // Only check for errors, not warnings + const hasErrors = Object.values(validationMessages).some((msg) => msg.severity === "error"); + Object.values(validationMessages).forEach((msg) => { + console.log(msg.message); + }); + return !hasErrors; }; const requiresInputFile = @@ -652,6 +692,15 @@ export const DataTierApplicationForm = () => { const showNewDatabase = operationType === DataTierOperationType.Import; const showApplicationInfo = operationType === DataTierOperationType.Extract; + async function handleFilePathChange(value: string): Promise { + setFilePath(value); + // Clear validation error when user types + const newMessages = { ...validationMessages }; + delete newMessages.filePath; + setValidationMessages(newMessages); + await validateFilePath(value, requiresInputFile); + } + return (
@@ -690,7 +739,7 @@ export const DataTierApplicationForm = () => { setOperationType(data.optionValue as DataTierOperationType); setErrorMessage(""); setSuccessMessage(""); - setValidationErrors({}); + setValidationMessages({}); }} disabled={isOperationInProgress}>
- {errorMessage && ( - - {errorMessage} - - )} - - {successMessage && ( - - {successMessage} - - )} - - {isOperationInProgress && ( -
- -
- )} -
Date: Wed, 22 Oct 2025 18:28:38 -0600 Subject: [PATCH 16/79] removing message panel --- .../dataTierApplicationForm.tsx | 46 +++---------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index b6282b48f2..ec2364e753 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -10,8 +10,6 @@ import { Input, Label, makeStyles, - MessageBar, - MessageBarBody, Option, Radio, RadioGroup, @@ -145,9 +143,6 @@ export const DataTierApplicationForm = () => { const [applicationName, setApplicationName] = useState(""); const [applicationVersion, setApplicationVersion] = useState(DEFAULT_APPLICATION_VERSION); const [isOperationInProgress, setIsOperationInProgress] = useState(false); - const [progressMessage, setProgressMessage] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); - const [successMessage, setSuccessMessage] = useState(""); const [validationMessages, setValidationMessages] = useState>( {}, ); @@ -243,14 +238,14 @@ export const DataTierApplicationForm = () => { ), ); } else { - setErrorMessage( + console.error( connectResult?.errorMessage || locConstants.dataTierApplication.connectionFailed, ); } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - setErrorMessage( + console.error( `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, ); } finally { @@ -280,8 +275,6 @@ export const DataTierApplicationForm = () => { const handleServerChange = async (profileId: string) => { setSelectedProfileId(profileId); - setErrorMessage(""); - setSuccessMessage(""); setValidationMessages({}); setIsConnecting(true); @@ -313,15 +306,13 @@ export const DataTierApplicationForm = () => { ); // Databases will be loaded automatically via useEffect } else { - setErrorMessage( + console.error( result?.errorMessage || locConstants.dataTierApplication.connectionFailed, ); } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - setErrorMessage( - `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, - ); + console.error(`${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`); } finally { setIsConnecting(false); } @@ -510,8 +501,6 @@ export const DataTierApplicationForm = () => { }; const handleSubmit = async () => { - setErrorMessage(""); - setSuccessMessage(""); setIsOperationInProgress(true); try { @@ -526,7 +515,6 @@ export const DataTierApplicationForm = () => { setIsOperationInProgress(false); return; } - setProgressMessage(locConstants.dataTierApplication.deployingDacpac); result = await context?.extensionRpc?.sendRequest( DeployDacpacWebviewRequest.type, { @@ -546,7 +534,6 @@ export const DataTierApplicationForm = () => { setIsOperationInProgress(false); return; } - setProgressMessage(locConstants.dataTierApplication.extractingDacpac); result = await context?.extensionRpc?.sendRequest( ExtractDacpacWebviewRequest.type, { @@ -567,7 +554,6 @@ export const DataTierApplicationForm = () => { setIsOperationInProgress(false); return; } - setProgressMessage(locConstants.dataTierApplication.importingBacpac); result = await context?.extensionRpc?.sendRequest( ImportBacpacWebviewRequest.type, { @@ -586,7 +572,6 @@ export const DataTierApplicationForm = () => { setIsOperationInProgress(false); return; } - setProgressMessage(locConstants.dataTierApplication.exportingBacpac); result = await context?.extensionRpc?.sendRequest( ExportBacpacWebviewRequest.type, { @@ -599,24 +584,20 @@ export const DataTierApplicationForm = () => { } if (result?.success) { - setSuccessMessage(getSuccessMessage(operationType)); - setProgressMessage(""); setIsOperationInProgress(false); clearForm(); } else { - setErrorMessage( + console.error( result?.errorMessage || locConstants.dataTierApplication.operationFailed, ); - setProgressMessage(""); setIsOperationInProgress(false); } } catch (error) { - setErrorMessage( + console.error( error instanceof Error ? error.message : locConstants.dataTierApplication.unexpectedError, ); - setProgressMessage(""); setIsOperationInProgress(false); } }; @@ -661,19 +642,6 @@ export const DataTierApplicationForm = () => { ); }; - const getSuccessMessage = (type: DataTierOperationType): string => { - switch (type) { - case DataTierOperationType.Deploy: - return locConstants.dataTierApplication.deploySuccess; - case DataTierOperationType.Extract: - return locConstants.dataTierApplication.extractSuccess; - case DataTierOperationType.Import: - return locConstants.dataTierApplication.importSuccess; - case DataTierOperationType.Export: - return locConstants.dataTierApplication.exportSuccess; - } - }; - const getOperationDescription = (type: DataTierOperationType): string => { switch (type) { case DataTierOperationType.Deploy: @@ -734,8 +702,6 @@ export const DataTierApplicationForm = () => { selectedOptions={[operationType]} onOptionSelect={(_, data) => { setOperationType(data.optionValue as DataTierOperationType); - setErrorMessage(""); - setSuccessMessage(""); setValidationMessages({}); }} disabled={isOperationInProgress}> From ea0ee8e3a905e0671e4c134a5cdd22c61f554556 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 22 Oct 2025 19:39:19 -0600 Subject: [PATCH 17/79] changes on operation component --- localization/l10n/bundle.l10n.json | 8 +- src/reactviews/common/locConstants.ts | 14 +++- .../dataTierApplicationForm.tsx | 73 ++++++++++--------- 3 files changed, 54 insertions(+), 41 deletions(-) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index 4d113084de..90b83bcd0a 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -737,10 +737,10 @@ "Extract DACPAC": "Extract DACPAC", "Import BACPAC": "Import BACPAC", "Export BACPAC": "Export BACPAC", - "Deploy a DACPAC to create or update a database": "Deploy a DACPAC to create or update a database", - "Extract a DACPAC from an existing database": "Extract a DACPAC from an existing database", - "Import a BACPAC to create a new database": "Import a BACPAC to create a new database", - "Export a BACPAC from an existing database": "Export a BACPAC from an existing database", + "Deploy a data-tier application .dacpac file to an instance of SQL Server": "Deploy a data-tier application .dacpac file to an instance of SQL Server", + "Extract a data-tier application .dacpac from an instance of SQL Server to a .dacpac file": "Extract a data-tier application .dacpac from an instance of SQL Server to a .dacpac file", + "Create a database from a .bacpac file": "Create a database from a .bacpac file", + "Export the schema and data from a database to the logical .bacpac file format": "Export the schema and data from a database to the logical .bacpac file format", "Package file": "Package file", "Output file": "Output file", "Select package file": "Select package file", diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index 8885237caf..5985c851d2 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -1073,10 +1073,16 @@ export class LocConstants { extractDacpac: l10n.t("Extract DACPAC"), importBacpac: l10n.t("Import BACPAC"), exportBacpac: l10n.t("Export BACPAC"), - deployDescription: l10n.t("Deploy a DACPAC to create or update a database"), - extractDescription: l10n.t("Extract a DACPAC from an existing database"), - importDescription: l10n.t("Import a BACPAC to create a new database"), - exportDescription: l10n.t("Export a BACPAC from an existing database"), + deployDescription: l10n.t( + "Deploy a data-tier application .dacpac file to an instance of SQL Server", + ), + extractDescription: l10n.t( + "Extract a data-tier application .dacpac from an instance of SQL Server to a .dacpac file", + ), + importDescription: l10n.t("Create a database from a .bacpac file"), + exportDescription: l10n.t( + "Export the schema and data from a database to the logical .bacpac file format", + ), packageFileLabel: l10n.t("Package file"), outputFileLabel: l10n.t("Output file"), selectPackageFile: l10n.t("Select package file"), diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index ec2364e753..def709e84e 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -642,19 +642,6 @@ export const DataTierApplicationForm = () => { ); }; - const getOperationDescription = (type: DataTierOperationType): string => { - switch (type) { - case DataTierOperationType.Deploy: - return locConstants.dataTierApplication.deployDescription; - case DataTierOperationType.Extract: - return locConstants.dataTierApplication.extractDescription; - case DataTierOperationType.Import: - return locConstants.dataTierApplication.importDescription; - case DataTierOperationType.Export: - return locConstants.dataTierApplication.exportDescription; - } - }; - const isFormValid = () => { if (!filePath || !databaseName) return false; // Only check for errors, not warnings @@ -696,31 +683,51 @@ export const DataTierApplicationForm = () => {
- { - setOperationType(data.optionValue as DataTierOperationType); + onChange={(_, data) => { + setOperationType(data.value as DataTierOperationType); setValidationMessages({}); }} disabled={isOperationInProgress}> - - - - - + + + + + - -
From 35e4398f24a00e95b00ded19a5e806f3a6299a16 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 23 Oct 2025 09:33:52 -0600 Subject: [PATCH 18/79] missing localization changes --- localization/xliff/vscode-mssql.xlf | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 54cd9f4b55..2d4c5e5379 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -935,6 +935,9 @@ Create a SQL database in Fabric (Preview) + + Create a database from a .bacpac file + Create a new firewall rule @@ -1069,8 +1072,8 @@ Deploy DACPAC - - Deploy a DACPAC to create or update a database + + Deploy a data-tier application .dacpac file to an instance of SQL Server Deploy to Existing Database @@ -1415,8 +1418,8 @@ Export BACPAC - - Export a BACPAC from an existing database + + Export the schema and data from a database to the logical .bacpac file format Exporting BACPAC... @@ -1427,8 +1430,8 @@ Extract DACPAC - - Extract a DACPAC from an existing database + + Extract a data-tier application .dacpac from an instance of SQL Server to a .dacpac file Extracting DACPAC... @@ -1806,9 +1809,6 @@ Import BACPAC - - Import a BACPAC to create a new database - Importance From 8ec36a3cbdb219283da337e504b2db6652ac4e84 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 23 Oct 2025 09:39:14 -0600 Subject: [PATCH 19/79] localization after merge --- localization/xliff/vscode-mssql.xlf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 73561d313f..4f49398bb5 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -1397,12 +1397,12 @@ Execution Plan - - Existing Database - Existing Azure SQL logical server + + Existing Database + Existing SQL server @@ -1815,12 +1815,12 @@ Ignore Tenant - - Import BACPAC - Image tag + + Import BACPAC + Importance From f2195887261563c65bbb9cc4ea217b2156e50c9d Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 23 Oct 2025 14:49:24 -0600 Subject: [PATCH 20/79] refactor to move logic to controller --- .../dataTierApplicationWebviewController.ts | 183 +++++++++++ .../dataTierApplicationForm.tsx | 116 ++----- src/sharedInterfaces/dataTierApplication.ts | 23 ++ ...taTierApplicationWebviewController.test.ts | 298 ++++++++++++++++++ 4 files changed, 532 insertions(+), 88 deletions(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index 0ebe7e94db..a0855d5e24 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -32,6 +32,7 @@ import { ExtractDacpacWebviewRequest, ImportBacpacParams, ImportBacpacWebviewRequest, + InitializeConnectionWebviewRequest, ListConnectionsWebviewRequest, ListDatabasesWebviewRequest, ValidateDatabaseNameWebviewRequest, @@ -145,6 +146,19 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr return await this.listConnections(); }); + // Initialize connection request handler + this.onRequest( + InitializeConnectionWebviewRequest.type, + async (params: { + initialServerName?: string; + initialDatabaseName?: string; + initialOwnerUri?: string; + initialProfileId?: string; + }) => { + return await this.initializeConnection(params); + }, + ); + // Connect to server request handler this.onRequest( ConnectToServerWebviewRequest.type, @@ -508,6 +522,175 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr } } + /** + * Initializes connection based on initial state from Object Explorer or previous session + * Handles auto-matching and auto-connecting to provide seamless user experience + */ + private async initializeConnection(params: { + initialServerName?: string; + initialDatabaseName?: string; + initialOwnerUri?: string; + initialProfileId?: string; + }): Promise<{ + connections: ConnectionProfile[]; + selectedConnection?: ConnectionProfile; + ownerUri?: string; + autoConnected: boolean; + errorMessage?: string; + }> { + try { + // Get all connections (recent + active) + const { connections } = await this.listConnections(); + + // Helper to find matching connection + const findMatchingConnection = (): ConnectionProfile | undefined => { + // Priority 1: Match by profile ID if provided + if (params.initialProfileId) { + const byProfileId = connections.find( + (conn) => conn.profileId === params.initialProfileId, + ); + if (byProfileId) { + this.logger.verbose( + `Found connection by profile ID: ${params.initialProfileId}`, + ); + return byProfileId; + } + } + + // Priority 2: Match by server name and database + if (params.initialServerName) { + const byServerAndDb = connections.find((conn) => { + const serverMatches = conn.server === params.initialServerName; + const databaseMatches = + !params.initialDatabaseName || + !conn.database || + conn.database === params.initialDatabaseName; + return serverMatches && databaseMatches; + }); + if (byServerAndDb) { + this.logger.verbose( + `Found connection by server/database: ${params.initialServerName}/${params.initialDatabaseName || "default"}`, + ); + return byServerAndDb; + } + } + + return undefined; + }; + + const matchingConnection = findMatchingConnection(); + + if (!matchingConnection) { + // No match found - return all connections, let user choose + this.logger.verbose("No matching connection found in initial state"); + return { + connections, + autoConnected: false, + }; + } + + // Found a matching connection + let ownerUri = params.initialOwnerUri; + let updatedConnections = connections; + + // Case 1: Already connected via Object Explorer (ownerUri provided) + if (params.initialOwnerUri) { + this.logger.verbose( + `Using existing connection from Object Explorer: ${params.initialOwnerUri}`, + ); + // Mark as connected if not already + if (!matchingConnection.isConnected) { + updatedConnections = connections.map((conn) => + conn.profileId === matchingConnection.profileId + ? { ...conn, isConnected: true } + : conn, + ); + } + return { + connections: updatedConnections, + selectedConnection: { ...matchingConnection, isConnected: true }, + ownerUri: params.initialOwnerUri, + autoConnected: false, // Was already connected + }; + } + + // Case 2: Connection exists but not connected - auto-connect + if (!matchingConnection.isConnected) { + this.logger.verbose(`Auto-connecting to profile: ${matchingConnection.profileId}`); + try { + const connectResult = await this.connectToServer(matchingConnection.profileId); + + if (connectResult.isConnected && connectResult.ownerUri) { + ownerUri = connectResult.ownerUri; + updatedConnections = connections.map((conn) => + conn.profileId === matchingConnection.profileId + ? { ...conn, isConnected: true } + : conn, + ); + this.logger.info( + `Successfully auto-connected to: ${matchingConnection.server}`, + ); + return { + connections: updatedConnections, + selectedConnection: { ...matchingConnection, isConnected: true }, + ownerUri, + autoConnected: true, + }; + } else { + // Connection failed + this.logger.error( + `Auto-connect failed: ${connectResult.errorMessage || "Unknown error"}`, + ); + return { + connections, + selectedConnection: matchingConnection, + autoConnected: false, + errorMessage: connectResult.errorMessage, + }; + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + this.logger.error(`Auto-connect exception: ${errorMsg}`); + return { + connections, + selectedConnection: matchingConnection, + autoConnected: false, + errorMessage: errorMsg, + }; + } + } + + // Case 3: Connection already active - fetch ownerUri + this.logger.verbose( + `Connection already active, fetching ownerUri for: ${matchingConnection.profileId}`, + ); + try { + const connectResult = await this.connectToServer(matchingConnection.profileId); + if (connectResult.ownerUri) { + ownerUri = connectResult.ownerUri; + this.logger.verbose(`Fetched ownerUri: ${ownerUri}`); + } + } catch (error) { + this.logger.error(`Failed to fetch ownerUri: ${error}`); + } + + return { + connections, + selectedConnection: matchingConnection, + ownerUri, + autoConnected: false, // Was already connected + }; + } catch (error) { + this.logger.error(`Failed to initialize connection: ${error}`); + // Fallback: return empty state + return { + connections: [], + autoConnected: false, + errorMessage: error instanceof Error ? error.message : String(error), + }; + } + } + /** * Connects to a server using the specified profile ID */ diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index def709e84e..02082c6566 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -28,7 +28,7 @@ import { ExtractDacpacWebviewRequest, ImportBacpacWebviewRequest, ExportBacpacWebviewRequest, - ListConnectionsWebviewRequest, + InitializeConnectionWebviewRequest, ValidateFilePathWebviewRequest, ListDatabasesWebviewRequest, ValidateDatabaseNameWebviewRequest, @@ -172,104 +172,44 @@ export const DataTierApplicationForm = () => { const loadConnections = async () => { try { + setIsConnecting(true); + const result = await context?.extensionRpc?.sendRequest( - ListConnectionsWebviewRequest.type, - undefined, + InitializeConnectionWebviewRequest.type, + { + initialServerName, + initialDatabaseName, + initialOwnerUri, + initialProfileId: initialSelectedProfileId, + }, ); - if (result?.connections) { + + if (result) { + // Set all available connections setAvailableConnections(result.connections); - const findMatchingConnection = (): ConnectionProfile | undefined => { - if (initialSelectedProfileId) { - const byProfileId = result.connections.find( - (conn) => conn.profileId === initialSelectedProfileId, - ); - if (byProfileId) { - return byProfileId; - } - } + // If a connection was selected/matched + if (result.selectedConnection) { + setSelectedProfileId(result.selectedConnection.profileId); - if (initialServerName) { - return result.connections.find((conn) => { - const serverMatches = conn.server === initialServerName; - const databaseMatches = - !initialDatabaseName || - !conn.database || - conn.database === initialDatabaseName; - return serverMatches && databaseMatches; - }); + // If we have an ownerUri (either provided or from auto-connect) + if (result.ownerUri) { + setOwnerUri(result.ownerUri); } - return undefined; - }; - - const matchingConnection = findMatchingConnection(); - - if (matchingConnection) { - setSelectedProfileId(matchingConnection.profileId); - - if (initialOwnerUri) { - // Already connected via Object Explorer - setOwnerUri(initialOwnerUri); - if (!matchingConnection.isConnected) { - setAvailableConnections((prev) => - prev.map((conn) => - conn.profileId === matchingConnection.profileId - ? { ...conn, isConnected: true } - : conn, - ), - ); - } - } else if (!matchingConnection.isConnected) { - setIsConnecting(true); - try { - const connectResult = await context?.extensionRpc?.sendRequest( - ConnectToServerWebviewRequest.type, - { profileId: matchingConnection.profileId }, - ); - - if (connectResult?.isConnected && connectResult.ownerUri) { - setOwnerUri(connectResult.ownerUri); - setAvailableConnections((prev) => - prev.map((conn) => - conn.profileId === matchingConnection.profileId - ? { ...conn, isConnected: true } - : conn, - ), - ); - } else { - console.error( - connectResult?.errorMessage || - locConstants.dataTierApplication.connectionFailed, - ); - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error( - `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, - ); - } finally { - setIsConnecting(false); - } - } else { - // Already connected, fetch ownerUri to ensure we have it - try { - const connectResult = await context?.extensionRpc?.sendRequest( - ConnectToServerWebviewRequest.type, - { profileId: matchingConnection.profileId }, - ); - - if (connectResult?.ownerUri) { - setOwnerUri(connectResult.ownerUri); - } - } catch (error) { - console.error("Failed to get ownerUri:", error); - } + // Show error if auto-connect failed + if (result.errorMessage && !result.autoConnected) { + console.error( + `${locConstants.dataTierApplication.connectionFailed}: ${result.errorMessage}`, + ); } } } } catch (error) { - console.error("Failed to load connections:", error); + const errorMsg = error instanceof Error ? error.message : String(error); + console.error(`${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`); + } finally { + setIsConnecting(false); } }; diff --git a/src/sharedInterfaces/dataTierApplication.ts b/src/sharedInterfaces/dataTierApplication.ts index e56c57deda..ddcd6c48f7 100644 --- a/src/sharedInterfaces/dataTierApplication.ts +++ b/src/sharedInterfaces/dataTierApplication.ts @@ -240,6 +240,29 @@ export namespace ListConnectionsWebviewRequest { ); } +/** + * Request to initialize connection based on initial state + * This handles auto-matching and auto-connecting if needed + */ +export namespace InitializeConnectionWebviewRequest { + export const type = new RequestType< + { + initialServerName?: string; + initialDatabaseName?: string; + initialOwnerUri?: string; + initialProfileId?: string; + }, + { + connections: ConnectionProfile[]; + selectedConnection?: ConnectionProfile; + ownerUri?: string; + autoConnected: boolean; + errorMessage?: string; + }, + void + >("dataTierApplication/initializeConnection"); +} + /** * Request to connect to a server from the webview */ diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index c4a5ddb29d..cf6754a040 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -22,6 +22,7 @@ import { ExportBacpacWebviewRequest, ExtractDacpacWebviewRequest, ImportBacpacWebviewRequest, + InitializeConnectionWebviewRequest, ListConnectionsWebviewRequest, ListDatabasesWebviewRequest, ValidateDatabaseNameWebviewRequest, @@ -1390,4 +1391,301 @@ suite("DataTierApplicationWebviewController", () => { expect(result.errorMessage).to.include("No active connection"); }); }); + + suite("Initialize Connection", () => { + let connStoreStub: sinon.SinonStubbedInstance; + + setup(() => { + connStoreStub = sandbox.createStubInstance(ConnectionStore); + sandbox.stub(connectionManagerStub, "connectionStore").get(() => connStoreStub); + }); + + test("returns all connections when no initial state provided", async () => { + // Setup + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 2, // SqlLogin + user: "sa", + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + expect(handler).to.exist; + + const result = await handler!({}); + + // Verify + expect(result).to.exist; + expect(result.connections).to.have.lengthOf(1); + expect(result.autoConnected).to.be.false; + expect(result.selectedConnection).to.be.undefined; + }); + + test("matches and returns connection by profile ID", async () => { + // Setup + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 2, // SqlLogin + }, + { + id: "profile2", + server: "server2", + authenticationType: 1, // Integrated + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialProfileId: "profile2", + }); + + // Verify + expect(result.selectedConnection).to.exist; + expect(result.selectedConnection?.server).to.equal("server2"); + expect(result.autoConnected).to.be.false; + }); + + test("matches connection by server and database names", async () => { + // Setup + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 2, // SqlLogin + }, + { + id: "profile2", + server: "testserver", + database: "testdb", + authenticationType: 1, // Integrated + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialServerName: "testserver", + initialDatabaseName: "testdb", + }); + + // Verify + expect(result.selectedConnection).to.exist; + expect(result.selectedConnection?.server).to.equal("testserver"); + expect(result.selectedConnection?.database).to.equal("testdb"); + }); + + test("uses existing ownerUri when provided from Object Explorer", async () => { + // Setup + const testOwnerUri = "test-owner-uri"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 2, // SqlLogin + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialServerName: "server1", + initialOwnerUri: testOwnerUri, + }); + + // Verify + expect(result.selectedConnection).to.exist; + expect(result.ownerUri).to.equal(testOwnerUri); + expect(result.autoConnected).to.be.false; // Was already connected + }); + + test("auto-connects when matching connection is not active", async () => { + // Setup + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 2, // SqlLogin + user: "sa", + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + connectionManagerStub.getUriForConnection.returns("new-owner-uri"); + connectionManagerStub.connect.resolves(true); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialProfileId: "profile1", + }); + + // Verify + expect(result.selectedConnection).to.exist; + expect(result.autoConnected).to.be.true; + expect(result.ownerUri).to.equal("new-owner-uri"); + expect(connectionManagerStub.connect).to.have.been.calledOnce; + }); + + test("returns error when auto-connect fails", async () => { + // Setup + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + authenticationType: 2, // SqlLogin + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + connectionManagerStub.connect.resolves(false); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialProfileId: "profile1", + }); + + // Verify + expect(result.autoConnected).to.be.false; + expect(result.errorMessage).to.exist; + }); + + test("fetches ownerUri for already active connection", async () => { + // Setup + const activeOwnerUri = "active-owner-uri"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 1, // Integrated + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({ + [activeOwnerUri]: { + credentials: recentConns[0], + serverInfo: {}, + }, + })); + connectionManagerStub.getUriForConnection.returns(activeOwnerUri); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialServerName: "server1", + }); + + // Verify + expect(result.selectedConnection).to.exist; + expect(result.ownerUri).to.equal(activeOwnerUri); + expect(result.autoConnected).to.be.false; // Already was connected + }); + + test("handles exception during initialization gracefully", async () => { + // Setup + connStoreStub.getRecentlyUsedConnections.throws(new Error("Store error")); + + createController(); + + // Execute + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + expect(handler, "InitializeConnection handler should be registered").to.exist; + + const result = await handler!({ + initialProfileId: "profile1", + }); + + // Verify - when store fails, listConnections catches it and returns empty array + // initializeConnection then returns empty array with no match found + expect(result.connections).to.exist; + expect(result.connections).to.be.an("array").that.is.empty; + expect(result.autoConnected).to.be.false; + expect(result.selectedConnection).to.be.undefined; + // No error message is set because listConnections handles the error internally + }); + + test("prioritizes profile ID match over server name match", async () => { + // Setup + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const recentConns: any[] = [ + { + id: "profile1", + server: "server1", + database: "db1", + authenticationType: 2, // SqlLogin + }, + { + id: "profile2", + server: "server1", + database: "db2", + authenticationType: 1, // Integrated + }, + ]; + + connStoreStub.getRecentlyUsedConnections.returns(recentConns); + sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); + + createController(); + + // Execute - both profile ID and server name match, profile ID should win + const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); + const result = await handler!({ + initialProfileId: "profile2", + initialServerName: "server1", + }); + + // Verify - should match profile2 by ID, not profile1 by server + expect(result.selectedConnection).to.exist; + expect(result.selectedConnection?.profileId).to.equal("profile2"); + expect(result.selectedConnection?.database).to.equal("db2"); + }); + }); }); From b2fad9586a765e5410c30e865e058f8cb361a2b0 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 23 Oct 2025 15:24:45 -0600 Subject: [PATCH 21/79] fix to clear databases when selected an non-active server --- .../dataTierApplicationForm.tsx | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 02082c6566..9e33e33837 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -216,7 +216,6 @@ export const DataTierApplicationForm = () => { const handleServerChange = async (profileId: string) => { setSelectedProfileId(profileId); setValidationMessages({}); - setIsConnecting(true); // Find the selected connection const selectedConnection = availableConnections.find( @@ -227,10 +226,11 @@ export const DataTierApplicationForm = () => { return; } - // If not connected, connect to the server - if (!selectedConnection.isConnected) { - setIsConnecting(true); - try { + setIsConnecting(true); + + try { + // If not connected, connect to the server + if (!selectedConnection.isConnected) { const result = await context?.extensionRpc?.sendRequest( ConnectToServerWebviewRequest.type, { profileId }, @@ -246,21 +246,22 @@ export const DataTierApplicationForm = () => { ); // Databases will be loaded automatically via useEffect } else { + // Connection failed - clear state + setOwnerUri(""); + setAvailableDatabases([]); + setDatabaseName(""); + // Ensure connection is marked as not connected + setAvailableConnections((prev) => + prev.map((conn) => + conn.profileId === profileId ? { ...conn, isConnected: false } : conn, + ), + ); console.error( result?.errorMessage || locConstants.dataTierApplication.connectionFailed, ); } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error(`${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`); - } finally { - setIsConnecting(false); - } - } else { - // Already connected, just need to get the ownerUri - // For now, we'll need to trigger a connection to get the ownerUri - // In a future enhancement, we could store ownerUri in the connection profile - try { + } else { + // Already connected, just need to get the ownerUri const result = await context?.extensionRpc?.sendRequest( ConnectToServerWebviewRequest.type, { profileId }, @@ -269,9 +270,12 @@ export const DataTierApplicationForm = () => { if (result?.ownerUri) { setOwnerUri(result.ownerUri); } - } catch (error) { - console.error("Failed to get ownerUri:", error); } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error(`${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`); + } finally { + setIsConnecting(false); } }; @@ -806,7 +810,7 @@ export const DataTierApplicationForm = () => { onOptionSelect={(_, data) => setDatabaseName(data.optionText || "") } - disabled={isOperationInProgress}> + disabled={isOperationInProgress || !ownerUri}> {availableDatabases.map((db) => (
- + {isConnecting ? ( { @@ -828,9 +895,13 @@ export const DataTierApplicationForm = () => { From a9c794c9f82284b16a01a0577a9e5cc14d58e448 Mon Sep 17 00:00:00 2001 From: allancascante Date: Fri, 24 Oct 2025 12:13:56 -0600 Subject: [PATCH 23/79] updates to localization --- localization/l10n/bundle.l10n.json | 1 + localization/xliff/vscode-mssql.xlf | 3 +++ 2 files changed, 4 insertions(+) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index 0a78ffd9cc..406f6bd6cf 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -774,6 +774,7 @@ "Exporting BACPAC...": "Exporting BACPAC...", "Operation failed": "Operation failed", "An unexpected error occurred": "An unexpected error occurred", + "Failed to load databases": "Failed to load databases", "DACPAC deployed successfully": "DACPAC deployed successfully", "DACPAC extracted successfully": "DACPAC extracted successfully", "BACPAC imported successfully": "BACPAC imported successfully", diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 4f49398bb5..232de223c2 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -1546,6 +1546,9 @@ {0} is the tenant id {1} is the account name + + Failed to load databases + Failed to open scmp file: '{0}' {0} is the error message returned from the open scmp operation From 6056efd216ccea55ff4098c7c4eb426fefba7d6d Mon Sep 17 00:00:00 2001 From: allancascante Date: Fri, 24 Oct 2025 14:39:32 -0600 Subject: [PATCH 24/79] unit tests updates --- ...taTierApplicationWebviewController.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index cf6754a040..ba80de3acf 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -959,6 +959,18 @@ suite("DataTierApplicationWebviewController", () => { .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); + // Stub getUriForConnection and isConnected for the test + connectionManagerStub.getUriForConnection.callsFake((profile) => { + if ( + profile.server === "server1.database.windows.net" && + profile.database === "db1" + ) { + return "uri1"; + } + return undefined; + }); + connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); + createController(); const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); @@ -1112,6 +1124,7 @@ suite("DataTierApplicationWebviewController", () => { test("returns existing ownerUri when already connected", async () => { connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); connectionManagerStub.getUriForConnection.returns("existing-owner-uri"); + connectionManagerStub.isConnected.withArgs("existing-owner-uri").returns(true); // Mock that connection already exists const mockActiveConnections = { @@ -1204,6 +1217,15 @@ suite("DataTierApplicationWebviewController", () => { .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); + // Stub getUriForConnection and isConnected + connectionManagerStub.getUriForConnection.callsFake((profile) => { + if (profile.server === "localhost" && profile.database === "master") { + return "uri1"; + } + return undefined; + }); + connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); + createController(); const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); @@ -1235,6 +1257,15 @@ suite("DataTierApplicationWebviewController", () => { .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); + // Stub getUriForConnection and isConnected + connectionManagerStub.getUriForConnection.callsFake((profile) => { + if (profile.server === "server2.database.windows.net") { + return "uri1"; + } + return undefined; + }); + connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); + createController(); const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); @@ -1614,6 +1645,7 @@ suite("DataTierApplicationWebviewController", () => { }, })); connectionManagerStub.getUriForConnection.returns(activeOwnerUri); + connectionManagerStub.isConnected.withArgs(activeOwnerUri).returns(true); createController(); From df8fc7a76c639c529244d4c696b26ec7e1175a77 Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 27 Oct 2025 14:12:24 -0600 Subject: [PATCH 25/79] adding telemetry and aria-label to form --- .../dataTierApplicationWebviewController.ts | 59 ++- .../dataTierApplicationForm.tsx | 43 +- src/sharedInterfaces/telemetry.ts | 5 + ...taTierApplicationWebviewController.test.ts | 400 ++++++++++++++++++ 4 files changed, 498 insertions(+), 9 deletions(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index 3e347382cf..a1260b8edf 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -14,6 +14,8 @@ import * as vscodeMssql from "vscode-mssql"; import { ReactWebviewPanelController } from "./reactWebviewPanelController"; import VscodeWrapper from "./vscodeWrapper"; import * as LocConstants from "../constants/locConstants"; +import { startActivity } from "../telemetry/telemetry"; +import { TelemetryViews, TelemetryActions, ActivityStatus } from "../sharedInterfaces/telemetry"; import { BrowseInputFileWebviewRequest, BrowseOutputFileWebviewRequest, @@ -242,6 +244,15 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr private async handleDeployDacpac( params: DeployDacpacParams, ): Promise { + const activity = startActivity( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxDeployDacpac, + undefined, + { + isNewDatabase: params.isNewDatabase.toString(), + }, + ); + try { const result = await this.dacFxService.deployDacpac( params.packageFilePath, @@ -258,13 +269,20 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }; if (result.success) { + activity.end(ActivityStatus.Succeeded); + this.dialogResult.resolve(appResult); + } else { + activity.endFailed( + new Error(result.errorMessage || "Deploy operation failed"), + false, + ); this.dialogResult.resolve(appResult); - // Don't dispose immediately to allow user to see success message } return appResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + activity.endFailed(error instanceof Error ? error : new Error(errorMessage), false); return { success: false, errorMessage: errorMessage, @@ -278,6 +296,11 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr private async handleExtractDacpac( params: ExtractDacpacParams, ): Promise { + const activity = startActivity( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxExtractDacpac, + ); + try { const result = await this.dacFxService.extractDacpac( params.databaseName, @@ -295,12 +318,20 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }; if (result.success) { + activity.end(ActivityStatus.Succeeded); + this.dialogResult.resolve(appResult); + } else { + activity.endFailed( + new Error(result.errorMessage || "Extract operation failed"), + false, + ); this.dialogResult.resolve(appResult); } return appResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + activity.endFailed(error instanceof Error ? error : new Error(errorMessage), false); return { success: false, errorMessage: errorMessage, @@ -314,6 +345,11 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr private async handleImportBacpac( params: ImportBacpacParams, ): Promise { + const activity = startActivity( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxImportBacpac, + ); + try { const result = await this.dacFxService.importBacpac( params.packageFilePath, @@ -329,12 +365,20 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }; if (result.success) { + activity.end(ActivityStatus.Succeeded); + this.dialogResult.resolve(appResult); + } else { + activity.endFailed( + new Error(result.errorMessage || "Import operation failed"), + false, + ); this.dialogResult.resolve(appResult); } return appResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + activity.endFailed(error instanceof Error ? error : new Error(errorMessage), false); return { success: false, errorMessage: errorMessage, @@ -348,6 +392,11 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr private async handleExportBacpac( params: ExportBacpacParams, ): Promise { + const activity = startActivity( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxExportBacpac, + ); + try { const result = await this.dacFxService.exportBacpac( params.databaseName, @@ -363,12 +412,20 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }; if (result.success) { + activity.end(ActivityStatus.Succeeded); + this.dialogResult.resolve(appResult); + } else { + activity.endFailed( + new Error(result.errorMessage || "Export operation failed"), + false, + ); this.dialogResult.resolve(appResult); } return appResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); + activity.endFailed(error instanceof Error ? error : new Error(errorMessage), false); return { success: false, errorMessage: errorMessage, diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index 3a36b94a84..c42ca0463a 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -690,7 +690,8 @@ export const DataTierApplicationForm = () => { setOperationType(data.value as DataTierOperationType); setValidationMessages({}); }} - disabled={isOperationInProgress}> + disabled={isOperationInProgress} + aria-label={locConstants.dataTierApplication.operationLabel}> { locConstants.dataTierApplication.deployDacpac + ")" } + aria-label={locConstants.dataTierApplication.deployDacpac} /> { locConstants.dataTierApplication.extractDacpac + ")" } + aria-label={locConstants.dataTierApplication.extractDacpac} /> { locConstants.dataTierApplication.importBacpac + ")" } + aria-label={locConstants.dataTierApplication.importBacpac} /> { locConstants.dataTierApplication.exportBacpac + ")" } + aria-label={locConstants.dataTierApplication.exportBacpac} /> @@ -760,7 +765,8 @@ export const DataTierApplicationForm = () => { }} disabled={ isOperationInProgress || availableConnections.length === 0 - }> + } + aria-label={locConstants.dataTierApplication.serverLabel}> {availableConnections.length === 0 ? (
@@ -826,16 +838,19 @@ export const DataTierApplicationForm = () => { setIsNewDatabase(data.value === "new")} - className={classes.radioGroup}> + className={classes.radioGroup} + aria-label={locConstants.dataTierApplication.targetDatabaseLabel}> @@ -854,6 +869,7 @@ export const DataTierApplicationForm = () => { onChange={(_, data) => setDatabaseName(data.value)} placeholder={locConstants.dataTierApplication.enterDatabaseName} disabled={isOperationInProgress} + aria-label={locConstants.dataTierApplication.databaseNameLabel} />
) : ( @@ -877,7 +893,8 @@ export const DataTierApplicationForm = () => { onOptionSelect={(_, data) => setDatabaseName(data.optionText || "") } - disabled={isOperationInProgress || !ownerUri}> + disabled={isOperationInProgress || !ownerUri} + aria-label={locConstants.dataTierApplication.databaseNameLabel}> {availableDatabases.map((db) => (
@@ -967,14 +992,16 @@ export const DataTierApplicationForm = () => {
diff --git a/src/sharedInterfaces/telemetry.ts b/src/sharedInterfaces/telemetry.ts index 91567f7983..ad34d378be 100644 --- a/src/sharedInterfaces/telemetry.ts +++ b/src/sharedInterfaces/telemetry.ts @@ -32,6 +32,7 @@ export enum TelemetryViews { Connection = "Connection", Credential = "Credential", ConnectionManager = "ConnectionManager", + DataTierApplication = "DataTierApplication", } export enum TelemetryActions { @@ -153,6 +154,10 @@ export enum TelemetryActions { LoadFabricWorkspaces = "LoadFabricWorkspaces", LoadDatabases = "LoadDatabases", GetSqlAnalyticsEndpointUrlFromFabric = "GetSqlAnalyticsEndpointUrlFromFabric", + DacFxDeployDacpac = "DacFxDeployDacpac", + DacFxExtractDacpac = "DacFxExtractDacpac", + DacFxImportBacpac = "DacFxImportBacpac", + DacFxExportBacpac = "DacFxExportBacpac", } /** diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index ba80de3acf..9c64269c3c 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -44,6 +44,12 @@ import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; import { ConnectionStore } from "../../src/models/connectionStore"; import * as fs from "fs"; import * as path from "path"; +import * as telemetry from "../../src/telemetry/telemetry"; +import { + TelemetryViews, + TelemetryActions, + ActivityStatus, +} from "../../src/sharedInterfaces/telemetry"; chai.use(sinonChai); @@ -1720,4 +1726,398 @@ suite("DataTierApplicationWebviewController", () => { expect(result.selectedConnection?.database).to.equal("db2"); }); }); + + suite("Telemetry", () => { + let startActivityStub: sinon.SinonStub; + let endStub: sinon.SinonStub; + let endFailedStub: sinon.SinonStub; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockActivity: any; + + setup(() => { + endStub = sandbox.stub(); + endFailedStub = sandbox.stub(); + mockActivity = { + end: endStub, + endFailed: endFailedStub, + correlationId: "test-correlation-id", + startTime: 0, + update: sandbox.stub(), + }; + startActivityStub = sandbox.stub(telemetry, "startActivity").returns(mockActivity); + }); + + suite("Deploy DACPAC", () => { + test("sends telemetry on successful deploy to new database", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "operation-123", + }; + + dacFxServiceStub.deployDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "NewDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + // Verify startActivity was called with correct parameters (twice: once for Load, once for the operation) + expect(startActivityStub).to.have.been.calledTwice; + expect(startActivityStub.secondCall).to.have.been.calledWith( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxDeployDacpac, + undefined, + sinon.match({ isNewDatabase: "true" }), + ); + + // Verify end was called for success + expect(endStub).to.have.been.calledOnce; + expect(endStub).to.have.been.calledWith(ActivityStatus.Succeeded); + + // Verify endFailed was not called + expect(endFailedStub).to.not.have.been.called; + }); + + test("sends telemetry on deploy failure from service", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Deployment failed: Permission denied", + operationId: "operation-789", + }; + + dacFxServiceStub.deployDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "TestDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + // Verify startActivity was called (twice: once for Load, once for the operation) + expect(startActivityStub).to.have.been.calledTwice; + + // Verify endFailed was called for service failure + expect(endFailedStub).to.have.been.calledOnce; + expect(endFailedStub).to.have.been.calledWith(sinon.match.instanceOf(Error), false); + + // Verify end was not called + expect(endStub).to.not.have.been.called; + }); + + test("sends telemetry on deploy exception", async () => { + dacFxServiceStub.deployDacpac.rejects(new Error("Network timeout")); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\test\\database.dacpac", + databaseName: "TestDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + // Verify endFailed was called for exception + expect(endFailedStub).to.have.been.calledOnce; + expect(endFailedStub).to.have.been.calledWith(sinon.match.instanceOf(Error), false); + + expect(endStub).to.not.have.been.called; + }); + + test("does not include sensitive data in telemetry", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "operation-123", + }; + + dacFxServiceStub.deployDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\private\\database.dacpac", + databaseName: "SensitiveDatabase", + isNewDatabase: true, + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + // Verify that additional properties don't contain sensitive data + const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry + const additionalProps = startActivityCall.args[3]; + + // Should not contain database name or file path + expect(additionalProps).to.not.have.property("databaseName"); + expect(additionalProps).to.not.have.property("packageFilePath"); + + // Verify end call doesn't contain sensitive data + expect(endStub).to.have.been.calledOnce; + expect(endStub).to.have.been.calledWith(ActivityStatus.Succeeded); + expect(endStub.firstCall.args).to.have.lengthOf(1); // Only status, no additional properties + }); + }); + + suite("Extract DACPAC", () => { + test("sends telemetry on successful extract", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "extract-123", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + const params = { + databaseName: "SourceDatabase", + packageFilePath: "C:\\output\\database.dacpac", + applicationName: "MyApp", + applicationVersion: "1.0.0", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + // Verify telemetry was started + expect(startActivityStub).to.have.been.calledTwice; // Load + Extract + expect(startActivityStub.secondCall).to.have.been.calledWith( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxExtractDacpac, + ); + + // Verify end was called for success + expect(endStub).to.have.been.calledOnce; + expect(endFailedStub).to.not.have.been.called; + }); + + test("sends telemetry on extract failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Extraction failed: Database not found", + operationId: "extract-456", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + const params = { + databaseName: "NonExistentDatabase", + packageFilePath: "C:\\output\\database.dacpac", + applicationName: "MyApp", + applicationVersion: "1.0.0", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + expect(startActivityStub).to.have.been.calledTwice; // Load + Extract + expect(endFailedStub).to.have.been.calledOnce; + expect(endStub).to.not.have.been.called; + }); + + test("does not include sensitive data in telemetry", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "extract-123", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + const params = { + databaseName: "SensitiveDatabase", + packageFilePath: "C:\\private\\database.dacpac", + applicationName: "PrivateApp", + applicationVersion: "2.0.0", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + // Verify startActivity doesn't contain sensitive data + const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry + expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action + + // Verify end doesn't contain sensitive data + expect(endStub.firstCall.args).to.have.lengthOf(1); + }); + }); + + suite("Import BACPAC", () => { + test("sends telemetry on successful import", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "import-123", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\backup\\database.bacpac", + databaseName: "RestoredDatabase", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + expect(startActivityStub).to.have.been.calledTwice; // Load + Import + expect(startActivityStub.secondCall).to.have.been.calledWith( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxImportBacpac, + ); + + expect(endStub).to.have.been.calledOnce; + expect(endFailedStub).to.not.have.been.called; + }); + + test("sends telemetry on import failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Import failed: Corrupted BACPAC file", + operationId: "import-456", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\backup\\corrupted.bacpac", + databaseName: "TestDatabase", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + expect(startActivityStub).to.have.been.calledTwice; // Load + Import + expect(endFailedStub).to.have.been.calledOnce; + expect(endStub).to.not.have.been.called; + }); + + test("does not include sensitive data in telemetry", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "import-123", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\private\\database.bacpac", + databaseName: "PrivateDatabase", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry + expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action + expect(endStub.firstCall.args).to.have.lengthOf(1); + }); + }); + + suite("Export BACPAC", () => { + test("sends telemetry on successful export", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "export-123", + }; + + dacFxServiceStub.exportBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); + const params = { + databaseName: "SourceDatabase", + packageFilePath: "C:\\backup\\database.bacpac", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + expect(startActivityStub).to.have.been.calledTwice; // Load + Export + expect(startActivityStub.secondCall).to.have.been.calledWith( + TelemetryViews.DataTierApplication, + TelemetryActions.DacFxExportBacpac, + ); + + expect(endStub).to.have.been.calledOnce; + expect(endFailedStub).to.not.have.been.called; + }); + + test("sends telemetry on export failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Export failed: Insufficient permissions", + operationId: "export-456", + }; + + dacFxServiceStub.exportBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); + const params = { + databaseName: "ProtectedDatabase", + packageFilePath: "C:\\backup\\database.bacpac", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + expect(startActivityStub).to.have.been.calledTwice; // Load + Export + expect(endFailedStub).to.have.been.calledOnce; + expect(endStub).to.not.have.been.called; + }); + + test("does not include sensitive data in telemetry", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "export-123", + }; + + dacFxServiceStub.exportBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); + const params = { + databaseName: "ConfidentialDatabase", + packageFilePath: "C:\\secure\\database.bacpac", + ownerUri: ownerUri, + }; + + await requestHandler!(params); + + const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry + expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action + expect(endStub.firstCall.args).to.have.lengthOf(1); + }); + }); + }); }); From f0b6a2a85957e31ade4046e827b349369c68f25b Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 27 Oct 2025 14:27:02 -0600 Subject: [PATCH 26/79] updating test --- test/unit/dataTierApplicationWebviewController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index 9c64269c3c..3ae624d3ca 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -225,7 +225,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.success).to.be.false; expect(response.errorMessage).to.equal("Deployment failed: Permission denied"); - expect(resolveSpy).to.not.have.been.called; + expect(resolveSpy).to.have.been.called; }); test("deploy DACPAC handles exception", async () => { From 5e59d0c6124ba1358dd9d4891c39aa077346c2aa Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 27 Oct 2025 15:05:10 -0600 Subject: [PATCH 27/79] adding extra tests --- ...taTierApplicationWebviewController.test.ts | 197 ++++++++++++++++++ test/unit/mainController.test.ts | 42 ++++ 2 files changed, 239 insertions(+) diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index 3ae624d3ca..fd042a9257 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -27,6 +27,8 @@ import { ListDatabasesWebviewRequest, ValidateDatabaseNameWebviewRequest, ValidateFilePathWebviewRequest, + BrowseInputFileWebviewRequest, + BrowseOutputFileWebviewRequest, } from "../../src/sharedInterfaces/dataTierApplication"; import * as LocConstants from "../../src/constants/locConstants"; import { @@ -421,6 +423,201 @@ suite("DataTierApplicationWebviewController", () => { }); }); + suite("Browse File Operations", () => { + let showOpenDialogStub: sinon.SinonStub; + let showSaveDialogStub: sinon.SinonStub; + + setup(() => { + showOpenDialogStub = sinon.stub(vscode.window, "showOpenDialog"); + showSaveDialogStub = sinon.stub(vscode.window, "showSaveDialog"); + }); + + teardown(() => { + showOpenDialogStub.restore(); + showSaveDialogStub.restore(); + }); + + test("browse input file returns file path when file is selected", async () => { + const mockUri = vscode.Uri.file("C:\\test\\database.dacpac"); + showOpenDialogStub.resolves([mockUri]); + createController(); + + const requestHandler = requestHandlers.get(BrowseInputFileWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const response = await requestHandler!({ fileExtension: "dacpac" }); + + expect(showOpenDialogStub).to.have.been.calledOnce; + expect(response.filePath).to.equal(mockUri.fsPath); + }); + + test("browse input file returns undefined when no file is selected", async () => { + showOpenDialogStub.resolves(undefined); + createController(); + + const requestHandler = requestHandlers.get(BrowseInputFileWebviewRequest.type.method); + const response = await requestHandler!({ fileExtension: "dacpac" }); + + expect(showOpenDialogStub).to.have.been.calledOnce; + expect(response.filePath).to.be.undefined; + }); + + test("browse input file returns undefined when empty array is returned", async () => { + showOpenDialogStub.resolves([]); + createController(); + + const requestHandler = requestHandlers.get(BrowseInputFileWebviewRequest.type.method); + const response = await requestHandler!({ fileExtension: "bacpac" }); + + expect(showOpenDialogStub).to.have.been.calledOnce; + expect(response.filePath).to.be.undefined; + }); + + test("browse output file returns file path when file is selected", async () => { + const mockUri = vscode.Uri.file("C:\\output\\database.dacpac"); + showSaveDialogStub.resolves(mockUri); + createController(); + + const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); + expect(requestHandler, "Request handler was not registered").to.be.a("function"); + + const response = await requestHandler!({ + fileExtension: "dacpac", + defaultFileName: "database.dacpac", + }); + + expect(showSaveDialogStub).to.have.been.calledOnce; + expect(response.filePath).to.equal(mockUri.fsPath); + }); + + test("browse output file returns undefined when dialog is cancelled", async () => { + showSaveDialogStub.resolves(undefined); + createController(); + + const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); + const response = await requestHandler!({ + fileExtension: "bacpac", + }); + + expect(showSaveDialogStub).to.have.been.calledOnce; + expect(response.filePath).to.be.undefined; + }); + }); + + suite("Operation Response Properties", () => { + test("extract DACPAC response includes all required properties", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "extract-op-123", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + const params = { + databaseName: "TestDB", + packageFilePath: "C:\\output\\test.dacpac", + applicationName: "TestApp", + applicationVersion: "1.0.0", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response).to.have.property("success"); + expect(response).to.have.property("errorMessage"); + expect(response).to.have.property("operationId"); + expect(response.success).to.be.true; + expect(response.errorMessage).to.be.undefined; + expect(response.operationId).to.equal("extract-op-123"); + }); + + test("extract DACPAC response includes error details on failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "Database extraction failed", + operationId: "extract-op-456", + }; + + dacFxServiceStub.extractDacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); + const params = { + databaseName: "TestDB", + packageFilePath: "C:\\output\\test.dacpac", + applicationName: "TestApp", + applicationVersion: "1.0.0", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response).to.have.property("success"); + expect(response).to.have.property("errorMessage"); + expect(response).to.have.property("operationId"); + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("Database extraction failed"); + expect(response.operationId).to.equal("extract-op-456"); + }); + + test("import BACPAC response includes all required properties", async () => { + const mockResult: DacFxResult = { + success: true, + errorMessage: undefined, + operationId: "import-op-789", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\backup\\test.bacpac", + databaseName: "RestoredDB", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response).to.have.property("success"); + expect(response).to.have.property("errorMessage"); + expect(response).to.have.property("operationId"); + expect(response.success).to.be.true; + expect(response.errorMessage).to.be.undefined; + expect(response.operationId).to.equal("import-op-789"); + }); + + test("import BACPAC response includes error details on failure", async () => { + const mockResult: DacFxResult = { + success: false, + errorMessage: "BACPAC file is corrupted", + operationId: "import-op-101", + }; + + dacFxServiceStub.importBacpac.resolves(mockResult); + createController(); + + const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); + const params = { + packageFilePath: "C:\\backup\\corrupted.bacpac", + databaseName: "TestDB", + ownerUri: ownerUri, + }; + + const response = await requestHandler!(params); + + expect(response).to.have.property("success"); + expect(response).to.have.property("errorMessage"); + expect(response).to.have.property("operationId"); + expect(response.success).to.be.false; + expect(response.errorMessage).to.equal("BACPAC file is corrupted"); + expect(response.operationId).to.equal("import-op-101"); + }); + }); + suite("File Path Validation", () => { test("validates existing DACPAC file", async () => { fsExistsSyncStub.returns(true); diff --git a/test/unit/mainController.test.ts b/test/unit/mainController.test.ts index 419ef77677..7391dace6c 100644 --- a/test/unit/mainController.test.ts +++ b/test/unit/mainController.test.ts @@ -171,4 +171,46 @@ suite("MainController Tests", function () { mainController.onPublishDatabaseProject = originalHandler; } }); + + suite("Data-Tier Application Commands", () => { + test("cmdDataTierApplication command is registered", async () => { + const commands = await vscode.commands.getCommands(true); + assert.ok( + commands.includes(Constants.cmdDataTierApplication), + "Expected cmdDataTierApplication to be registered", + ); + }); + + test("cmdDeployDacpac command is registered", async () => { + const commands = await vscode.commands.getCommands(true); + assert.ok( + commands.includes(Constants.cmdDeployDacpac), + "Expected cmdDeployDacpac to be registered", + ); + }); + + test("cmdExtractDacpac command is registered", async () => { + const commands = await vscode.commands.getCommands(true); + assert.ok( + commands.includes(Constants.cmdExtractDacpac), + "Expected cmdExtractDacpac to be registered", + ); + }); + + test("cmdImportBacpac command is registered", async () => { + const commands = await vscode.commands.getCommands(true); + assert.ok( + commands.includes(Constants.cmdImportBacpac), + "Expected cmdImportBacpac to be registered", + ); + }); + + test("cmdExportBacpac command is registered", async () => { + const commands = await vscode.commands.getCommands(true); + assert.ok( + commands.includes(Constants.cmdExportBacpac), + "Expected cmdExportBacpac to be registered", + ); + }); + }); }); From 390ef10ecc4a26c4d137cf4ebaea20a5e8481803 Mon Sep 17 00:00:00 2001 From: allancascante Date: Fri, 31 Oct 2025 20:10:08 -0600 Subject: [PATCH 28/79] adding support to suggest names --- .../dataTierApplicationWebviewController.ts | 37 +++++ .../dataTierApplicationForm.tsx | 53 +++++++- src/sharedInterfaces/dataTierApplication.ts | 12 ++ ...taTierApplicationWebviewController.test.ts | 126 ++++++++++++++++++ 4 files changed, 227 insertions(+), 1 deletion(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index a1260b8edf..78c413ad1e 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -32,6 +32,7 @@ import { ExportBacpacWebviewRequest, ExtractDacpacParams, ExtractDacpacWebviewRequest, + GetSuggestedOutputPathWebviewRequest, ImportBacpacParams, ImportBacpacWebviewRequest, InitializeConnectionWebviewRequest, @@ -218,6 +219,42 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }, ); + // Get default output path without showing dialog + this.onRequest( + GetSuggestedOutputPathWebviewRequest.type, + async (params: { databaseName: string; operationType: DataTierOperationType }) => { + const fileExtension = + params.operationType === DataTierOperationType.Extract ? "dacpac" : "bacpac"; + + // Format timestamp as yyyy-MM-dd-HH-mm using Intl.DateTimeFormat + const now = new Date(); + const dateFormatter = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + const timeFormatter = new Intl.DateTimeFormat("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + const datePart = dateFormatter.format(now); // yyyy-MM-dd + const timePart = timeFormatter.format(now).replace(/:/g, "-"); // HH-mm + const timestamp = `${datePart}-${timePart}`; + + const suggestedFileName = `${params.databaseName}-${timestamp}.${fileExtension}`; + + // Get workspace folder or home directory + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri; + const defaultUri = workspaceFolder + ? vscode.Uri.joinPath(workspaceFolder, suggestedFileName) + : vscode.Uri.file(path.join(require("os").homedir(), suggestedFileName)); + + return { fullPath: defaultUri.fsPath }; + }, + ); + // Confirm deploy to existing database request handler this.onRequest(ConfirmDeployToExistingWebviewRequest.type, async () => { const result = await this.vscodeWrapper.showWarningMessageAdvanced( diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index c42ca0463a..c7f226ef8f 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -28,6 +28,7 @@ import { ExtractDacpacWebviewRequest, ImportBacpacWebviewRequest, ExportBacpacWebviewRequest, + GetSuggestedOutputPathWebviewRequest, InitializeConnectionWebviewRequest, ValidateFilePathWebviewRequest, ListDatabasesWebviewRequest, @@ -179,6 +180,33 @@ export const DataTierApplicationForm = () => { } }, [operationType, ownerUri]); + // Update file path suggestion when database or operation type changes for Export/Extract + useEffect(() => { + const updateSuggestedPath = async () => { + if ( + databaseName && + (operationType === DataTierOperationType.Extract || + operationType === DataTierOperationType.Export) && + context?.extensionRpc + ) { + // Get the suggested full path from the controller + const result = await context.extensionRpc.sendRequest( + GetSuggestedOutputPathWebviewRequest.type, + { + databaseName, + operationType, + }, + ); + + if (result?.fullPath) { + setFilePath(result.fullPath); + } + } + }; + + void updateSuggestedPath(); + }, [databaseName, operationType, context]); + const loadConnections = async () => { try { setIsConnecting(true); @@ -619,7 +647,30 @@ export const DataTierApplicationForm = () => { }); } else { // Browse for output file (Extract or Export) - const defaultFileName = `${initialDatabaseName || "database"}.${fileExtension}`; + // Use the suggested filename from state, or fallback to a default + let defaultFileName = filePath; + + if (!defaultFileName) { + // Generate default filename with timestamp using Intl.DateTimeFormat + const now = new Date(); + const dateFormatter = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + const timeFormatter = new Intl.DateTimeFormat("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + const datePart = dateFormatter.format(now); // yyyy-MM-dd + const timePart = timeFormatter.format(now).replace(/:/g, "-"); // HH-mm + const timestamp = `${datePart}-${timePart}`; + + defaultFileName = `${databaseName || "database"}-${timestamp}.${fileExtension}`; + } + result = await context?.extensionRpc?.sendRequest(BrowseOutputFileWebviewRequest.type, { fileExtension, defaultFileName, diff --git a/src/sharedInterfaces/dataTierApplication.ts b/src/sharedInterfaces/dataTierApplication.ts index ddcd6c48f7..0307411123 100644 --- a/src/sharedInterfaces/dataTierApplication.ts +++ b/src/sharedInterfaces/dataTierApplication.ts @@ -310,6 +310,18 @@ export namespace BrowseOutputFileWebviewRequest { >("dataTierApplication/browseOutputFile"); } +/** + * Request to get the suggested full path for an output file without showing dialog + * Generates path with timestamp based on database name and operation type + */ +export namespace GetSuggestedOutputPathWebviewRequest { + export const type = new RequestType< + { databaseName: string; operationType: DataTierOperationType }, + { fullPath: string }, + void + >("dataTierApplication/getSuggestedOutputPath"); +} + /** * Request to show a confirmation dialog for deploying to an existing database */ diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index fd042a9257..eb22b52506 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -21,6 +21,7 @@ import { DeployDacpacWebviewRequest, ExportBacpacWebviewRequest, ExtractDacpacWebviewRequest, + GetSuggestedOutputPathWebviewRequest, ImportBacpacWebviewRequest, InitializeConnectionWebviewRequest, ListConnectionsWebviewRequest, @@ -502,6 +503,131 @@ suite("DataTierApplicationWebviewController", () => { expect(showSaveDialogStub).to.have.been.calledOnce; expect(response.filePath).to.be.undefined; }); + + test("browse output file uses workspace folder as default location when available", async () => { + const workspaceFolder = vscode.Uri.file("/workspace/myproject"); + sandbox + .stub(vscode.workspace, "workspaceFolders") + .get(() => [{ uri: workspaceFolder, name: "myproject", index: 0 }]); + + const mockUri = vscode.Uri.file("/workspace/myproject/database.dacpac"); + showSaveDialogStub.resolves(mockUri); + createController(); + + const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); + await requestHandler!({ + fileExtension: "dacpac", + defaultFileName: "database.dacpac", + }); + + expect(showSaveDialogStub).to.have.been.calledOnce; + const options = showSaveDialogStub.firstCall.args[0]; + expect(options.defaultUri.fsPath).to.include("myproject"); + expect(options.defaultUri.fsPath).to.include("database.dacpac"); + }); + + test("browse output file falls back to home directory when no workspace folder available", async () => { + sandbox.stub(vscode.workspace, "workspaceFolders").get(() => undefined); + + const mockUri = vscode.Uri.file("/home/user/database.bacpac"); + showSaveDialogStub.resolves(mockUri); + createController(); + + const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); + await requestHandler!({ + fileExtension: "bacpac", + defaultFileName: "database.bacpac", + }); + + expect(showSaveDialogStub).to.have.been.calledOnce; + const options = showSaveDialogStub.firstCall.args[0]; + // Should use home directory - verify it doesn't contain workspace path + expect(options.defaultUri.fsPath).to.not.include("workspace"); + expect(options.defaultUri.fsPath).to.include("database.bacpac"); + }); + + test("get suggested output path generates full path with workspace folder", async () => { + const workspaceFolder = vscode.Uri.file("/workspace/myproject"); + sandbox + .stub(vscode.workspace, "workspaceFolders") + .get(() => [{ uri: workspaceFolder, name: "myproject", index: 0 }]); + + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedOutputPathWebviewRequest.type.method, + ); + const result = await requestHandler!({ + databaseName: "AdventureWorks", + operationType: DataTierOperationType.Extract, + }); + + expect(result.fullPath).to.include("myproject"); + expect(result.fullPath).to.include("AdventureWorks"); + expect(result.fullPath).to.include(".dacpac"); + // Should include timestamp in format yyyy-MM-dd-HH-mm + expect(result.fullPath).to.match(/\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/); + }); + + test("get suggested output path falls back to home directory when no workspace", async () => { + sandbox.stub(vscode.workspace, "workspaceFolders").get(() => undefined); + + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedOutputPathWebviewRequest.type.method, + ); + const result = await requestHandler!({ + databaseName: "TestDB", + operationType: DataTierOperationType.Export, + }); + + expect(result.fullPath).to.not.include("workspace"); + expect(result.fullPath).to.include("TestDB"); + expect(result.fullPath).to.include(".bacpac"); + // Should include timestamp in format yyyy-MM-dd-HH-mm + expect(result.fullPath).to.match(/\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/); + }); + + test("get suggested output path uses correct extension for Extract", async () => { + const workspaceFolder = vscode.Uri.file("/workspace/test"); + sandbox + .stub(vscode.workspace, "workspaceFolders") + .get(() => [{ uri: workspaceFolder, name: "test", index: 0 }]); + + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedOutputPathWebviewRequest.type.method, + ); + const result = await requestHandler!({ + databaseName: "MyDB", + operationType: DataTierOperationType.Extract, + }); + + expect(result.fullPath).to.include(".dacpac"); + expect(result.fullPath).to.not.include(".bacpac"); + }); + + test("get suggested output path uses correct extension for Export", async () => { + const workspaceFolder = vscode.Uri.file("/workspace/test"); + sandbox + .stub(vscode.workspace, "workspaceFolders") + .get(() => [{ uri: workspaceFolder, name: "test", index: 0 }]); + + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedOutputPathWebviewRequest.type.method, + ); + const result = await requestHandler!({ + databaseName: "MyDB", + operationType: DataTierOperationType.Export, + }); + + expect(result.fullPath).to.include(".bacpac"); + expect(result.fullPath).to.not.include(".dacpac"); + }); }); suite("Operation Response Properties", () => { From 655f14644c04ac5291a5529aeec132b22946529d Mon Sep 17 00:00:00 2001 From: allancascante Date: Fri, 31 Oct 2025 21:18:20 -0600 Subject: [PATCH 29/79] database name suggestions --- .../dataTierApplicationWebviewController.ts | 38 +++++++---- .../dataTierApplicationForm.tsx | 28 ++++++++ src/sharedInterfaces/dataTierApplication.ts | 10 +++ ...taTierApplicationWebviewController.test.ts | 68 +++++++++++++++++++ test/unit/mainController.test.ts | 2 +- 5 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dataTierApplicationWebviewController.ts index 78c413ad1e..828f557b69 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dataTierApplicationWebviewController.ts @@ -32,6 +32,7 @@ import { ExportBacpacWebviewRequest, ExtractDacpacParams, ExtractDacpacWebviewRequest, + GetSuggestedDatabaseNameWebviewRequest, GetSuggestedOutputPathWebviewRequest, ImportBacpacParams, ImportBacpacWebviewRequest, @@ -226,22 +227,14 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr const fileExtension = params.operationType === DataTierOperationType.Extract ? "dacpac" : "bacpac"; - // Format timestamp as yyyy-MM-dd-HH-mm using Intl.DateTimeFormat + // Format timestamp as yyyy-MM-dd-HH-mm const now = new Date(); - const dateFormatter = new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }); - const timeFormatter = new Intl.DateTimeFormat("en-US", { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); - - const datePart = dateFormatter.format(now); // yyyy-MM-dd - const timePart = timeFormatter.format(now).replace(/:/g, "-"); // HH-mm - const timestamp = `${datePart}-${timePart}`; + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const timestamp = `${year}-${month}-${day}-${hours}-${minutes}`; const suggestedFileName = `${params.databaseName}-${timestamp}.${fileExtension}`; @@ -255,6 +248,21 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr }, ); + // Get suggested database name from file path + this.onRequest( + GetSuggestedDatabaseNameWebviewRequest.type, + async (params: { filePath: string }) => { + // Extract filename without directory path + const fileName = path.basename(params.filePath); + + // Remove file extension (.dacpac or .bacpac) to get the database name + // Keep the full filename including any timestamps that may be present + const databaseName = fileName.replace(/\.(dacpac|bacpac)$/i, ""); + + return { databaseName }; + }, + ); + // Confirm deploy to existing database request handler this.onRequest(ConfirmDeployToExistingWebviewRequest.type, async () => { const result = await this.vscodeWrapper.showWarningMessageAdvanced( diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx index c7f226ef8f..eddfbe58a0 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx @@ -28,6 +28,7 @@ import { ExtractDacpacWebviewRequest, ImportBacpacWebviewRequest, ExportBacpacWebviewRequest, + GetSuggestedDatabaseNameWebviewRequest, GetSuggestedOutputPathWebviewRequest, InitializeConnectionWebviewRequest, ValidateFilePathWebviewRequest, @@ -685,6 +686,29 @@ export const DataTierApplicationForm = () => { setValidationMessages(newMessages); // Validate the selected file path await validateFilePath(result.filePath, requiresInputFile); + + // For Deploy/Import operations, suggest database name from the selected file + if ( + requiresInputFile && + context?.extensionRpc && + (operationType === DataTierOperationType.Deploy || + operationType === DataTierOperationType.Import) + ) { + const nameResult = await context.extensionRpc.sendRequest( + GetSuggestedDatabaseNameWebviewRequest.type, + { + filePath: result.filePath, + }, + ); + + if (nameResult?.databaseName) { + setDatabaseName(nameResult.databaseName); + // Clear any existing database name validation errors + const updatedMessages = { ...validationMessages }; + delete updatedMessages.databaseName; + setValidationMessages(updatedMessages); + } + } } }; @@ -740,6 +764,10 @@ export const DataTierApplicationForm = () => { onChange={(_, data) => { setOperationType(data.value as DataTierOperationType); setValidationMessages({}); + // Reset file path when switching operation types + // Import/Deploy need empty (browse for existing file) + // Export/Extract will be set when database name changes + setFilePath(""); }} disabled={isOperationInProgress} aria-label={locConstants.dataTierApplication.operationLabel}> diff --git a/src/sharedInterfaces/dataTierApplication.ts b/src/sharedInterfaces/dataTierApplication.ts index 0307411123..b9e0f0d592 100644 --- a/src/sharedInterfaces/dataTierApplication.ts +++ b/src/sharedInterfaces/dataTierApplication.ts @@ -322,6 +322,16 @@ export namespace GetSuggestedOutputPathWebviewRequest { >("dataTierApplication/getSuggestedOutputPath"); } +/** + * Request to get the suggested database name from a file path + * Extracts database name from the filename without extension or timestamps + */ +export namespace GetSuggestedDatabaseNameWebviewRequest { + export const type = new RequestType<{ filePath: string }, { databaseName: string }, void>( + "dataTierApplication/getSuggestedDatabaseName", + ); +} + /** * Request to show a confirmation dialog for deploying to an existing database */ diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index eb22b52506..7f9fd18868 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -21,6 +21,7 @@ import { DeployDacpacWebviewRequest, ExportBacpacWebviewRequest, ExtractDacpacWebviewRequest, + GetSuggestedDatabaseNameWebviewRequest, GetSuggestedOutputPathWebviewRequest, ImportBacpacWebviewRequest, InitializeConnectionWebviewRequest, @@ -628,6 +629,73 @@ suite("DataTierApplicationWebviewController", () => { expect(result.fullPath).to.include(".bacpac"); expect(result.fullPath).to.not.include(".dacpac"); }); + + test("get suggested database name removes extension from simple filename", async () => { + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedDatabaseNameWebviewRequest.type.method, + ); + const result = await requestHandler!({ + filePath: "C:\\files\\AdventureWorks.dacpac", + }); + + expect(result.databaseName).to.equal("AdventureWorks"); + }); + + test("get suggested database name keeps timestamp in filename", async () => { + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedDatabaseNameWebviewRequest.type.method, + ); + const result = await requestHandler!({ + filePath: "C:\\files\\AdventureWorks-2025-10-31-14-30.dacpac", + }); + + expect(result.databaseName).to.equal("AdventureWorks-2025-10-31-14-30"); + }); + + test("get suggested database name works with .bacpac extension", async () => { + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedDatabaseNameWebviewRequest.type.method, + ); + const result = await requestHandler!({ + filePath: "/home/user/MyDatabase.bacpac", + }); + + expect(result.databaseName).to.equal("MyDatabase"); + }); + + test("get suggested database name handles complex filename with multiple hyphens", async () => { + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedDatabaseNameWebviewRequest.type.method, + ); + const result = await requestHandler!({ + filePath: "C:\\exports\\My-Complex-Database-Name-2025-01-15-10-30.bacpac", + }); + + expect(result.databaseName).to.equal("My-Complex-Database-Name-2025-01-15-10-30"); + }); + + test("get suggested database name extracts only filename from full path", async () => { + createController(); + + const requestHandler = requestHandlers.get( + GetSuggestedDatabaseNameWebviewRequest.type.method, + ); + const result = await requestHandler!({ + filePath: "C:\\very\\long\\path\\to\\files\\TestDB.dacpac", + }); + + expect(result.databaseName).to.equal("TestDB"); + expect(result.databaseName).to.not.include("path"); + expect(result.databaseName).to.not.include("files"); + }); }); suite("Operation Response Properties", () => { diff --git a/test/unit/mainController.test.ts b/test/unit/mainController.test.ts index 66de0cca98..d4f59c81b9 100644 --- a/test/unit/mainController.test.ts +++ b/test/unit/mainController.test.ts @@ -5,7 +5,7 @@ import * as sinon from "sinon"; import sinonChai from "sinon-chai"; -import { expect } from "chai"; +import { expect, assert } from "chai"; import * as chai from "chai"; import * as vscode from "vscode"; import * as Extension from "../../src/extension"; From 60744d1fd074d83c7f943081ae7abc65c4890b90 Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 3 Nov 2025 09:15:35 -0600 Subject: [PATCH 30/79] adding localization and spinner --- localization/l10n/bundle.l10n.json | 2 +- localization/xliff/vscode-mssql.xlf | 9 ++++++--- src/reactviews/common/locConstants.ts | 1 + .../DataTierApplication/dataTierApplicationPage.tsx | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index 635d39a48c..26cad5731f 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -743,6 +743,7 @@ "Passwords do not match": "Passwords do not match", "Data-tier Application": "Data-tier Application", "Deploy, extract, import, or export data-tier applications on the selected database": "Deploy, extract, import, or export data-tier applications on the selected database", + "Loading...": "Loading...", "Operation": "Operation", "Select an operation": "Select an operation", "Select a server": "Select a server", @@ -1168,7 +1169,6 @@ "message": "Are you sure you want to remove {0}?", "comment": ["{0} is the node label"] }, - "Loading...": "Loading...", "Fetching {0} script.../{0} is the script type": { "message": "Fetching {0} script...", "comment": ["{0} is the script type"] diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 2632f2afe7..72652e5570 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -991,15 +991,15 @@ Custom Zoom - - DacFx service is not available - DACPAC deployed successfully DACPAC extracted successfully + + DacFx service is not available + Data Type @@ -1575,6 +1575,9 @@ Failed to list databases + + Failed to load databases + Failed to load publish profile diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index 8af1907167..368190c76e 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -1087,6 +1087,7 @@ export class LocConstants { subtitle: l10n.t( "Deploy, extract, import, or export data-tier applications on the selected database", ), + loading: l10n.t("Loading..."), operationLabel: l10n.t("Operation"), selectOperation: l10n.t("Select an operation"), serverLabel: l10n.t("Server"), diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx index 0166d9f20b..3a06e0298b 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx @@ -4,14 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { useContext } from "react"; +import { Spinner } from "@fluentui/react-components"; import { DataTierApplicationContext } from "./dataTierApplicationStateProvider"; import { DataTierApplicationForm } from "./dataTierApplicationForm"; +import { locConstants } from "../../common/locConstants"; export const DataTierApplicationPage = () => { const context = useContext(DataTierApplicationContext); if (!context) { - return
Loading...
; + return ; } return ; From 6c86f32234d691be892cbe2149158543a982b166 Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 3 Nov 2025 09:32:15 -0600 Subject: [PATCH 31/79] style changes to match example from pr --- .../dataTierApplicationPage.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx index 3a06e0298b..eb5f703ff7 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx @@ -13,7 +13,21 @@ export const DataTierApplicationPage = () => { const context = useContext(DataTierApplicationContext); if (!context) { - return ; +
+ +
; } return ; From 92d60714752bd3703739041f5dcb1d37e6cfb54d Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 3 Nov 2025 10:29:20 -0600 Subject: [PATCH 32/79] fix on tests --- ...taTierApplicationWebviewController.test.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dataTierApplicationWebviewController.test.ts index 7f9fd18868..30fe9fa9a8 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dataTierApplicationWebviewController.test.ts @@ -636,8 +636,12 @@ suite("DataTierApplicationWebviewController", () => { const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); + const testPath = path.join( + path.sep === "\\" ? "C:\\files" : "/files", + "AdventureWorks.dacpac", + ); const result = await requestHandler!({ - filePath: "C:\\files\\AdventureWorks.dacpac", + filePath: testPath, }); expect(result.databaseName).to.equal("AdventureWorks"); @@ -649,8 +653,12 @@ suite("DataTierApplicationWebviewController", () => { const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); + const testPath = path.join( + path.sep === "\\" ? "C:\\files" : "/files", + "AdventureWorks-2025-10-31-14-30.dacpac", + ); const result = await requestHandler!({ - filePath: "C:\\files\\AdventureWorks-2025-10-31-14-30.dacpac", + filePath: testPath, }); expect(result.databaseName).to.equal("AdventureWorks-2025-10-31-14-30"); @@ -662,8 +670,12 @@ suite("DataTierApplicationWebviewController", () => { const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); + const testPath = path.join( + path.sep === "\\" ? "C:\\user" : "/home/user", + "MyDatabase.bacpac", + ); const result = await requestHandler!({ - filePath: "/home/user/MyDatabase.bacpac", + filePath: testPath, }); expect(result.databaseName).to.equal("MyDatabase"); @@ -675,8 +687,12 @@ suite("DataTierApplicationWebviewController", () => { const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); + const testPath = path.join( + path.sep === "\\" ? "C:\\exports" : "/exports", + "My-Complex-Database-Name-2025-01-15-10-30.bacpac", + ); const result = await requestHandler!({ - filePath: "C:\\exports\\My-Complex-Database-Name-2025-01-15-10-30.bacpac", + filePath: testPath, }); expect(result.databaseName).to.equal("My-Complex-Database-Name-2025-01-15-10-30"); @@ -688,8 +704,12 @@ suite("DataTierApplicationWebviewController", () => { const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); + const testPath = path.join( + path.sep === "\\" ? "C:\\very\\long\\path\\to\\files" : "/very/long/path/to/files", + "TestDB.dacpac", + ); const result = await requestHandler!({ - filePath: "C:\\very\\long\\path\\to\\files\\TestDB.dacpac", + filePath: testPath, }); expect(result.databaseName).to.equal("TestDB"); From b81ec234c57c45a5396d5fc948349d484f7a1ebb Mon Sep 17 00:00:00 2001 From: allancascante Date: Mon, 3 Nov 2025 10:49:48 -0600 Subject: [PATCH 33/79] small fixes --- src/controllers/mainController.ts | 2 +- .../dataTierApplicationPage.tsx | 32 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 24bd6212c6..b2b9623a8f 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -1808,7 +1808,7 @@ export default class MainController implements vscode.Disposable { serverName, databaseName, selectedProfileId: profileId, - operationType: undefined, + operationType: DataTierOperationType.Deploy, }; const controller = new DataTierApplicationWebviewController( diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx index eb5f703ff7..40ce38e90e 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx +++ b/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx @@ -13,21 +13,23 @@ export const DataTierApplicationPage = () => { const context = useContext(DataTierApplicationContext); if (!context) { -
- -
; + return ( +
+ +
+ ); } return ; From 25c78dec9583fe79b705df448f4576984723ea01 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 10:24:56 -0600 Subject: [PATCH 34/79] renaming the whole functionality to match the service (dacFx) instead of data-tier application --- localization/xliff/vscode-mssql.xlf | 2 +- package.json | 6 +- package.nls.json | 2 +- scripts/bundle-reactviews.js | 2 +- src/constants/constants.ts | 2 +- src/constants/locConstants.ts | 2 +- ...s => dacFxApplicationWebviewController.ts} | 102 ++++---- src/controllers/mainController.ts | 40 +-- src/reactviews/common/locConstants.ts | 4 +- .../dacFxApplication.css} | 0 .../dacFxApplicationForm.tsx} | 230 +++++++++--------- .../dacFxApplicationPage.tsx} | 12 +- .../dacFxApplicationSelector.ts} | 8 +- .../dacFxApplicationStateProvider.tsx | 32 +++ .../index.tsx | 10 +- .../dataTierApplicationStateProvider.tsx | 34 --- .../components/SchemaSelectorDrawer.tsx | 2 +- ...TierApplication.ts => dacFxApplication.ts} | 64 ++--- src/sharedInterfaces/telemetry.ts | 2 +- ...dacFxApplicationWebviewController.test.ts} | 91 ++++--- test/unit/mainController.test.ts | 6 +- 21 files changed, 314 insertions(+), 339 deletions(-) rename src/controllers/{dataTierApplicationWebviewController.ts => dacFxApplicationWebviewController.ts} (91%) rename src/reactviews/pages/{DataTierApplication/dataTierApplication.css => DacFxApplication/dacFxApplication.css} (100%) rename src/reactviews/pages/{DataTierApplication/dataTierApplicationForm.tsx => DacFxApplication/dacFxApplicationForm.tsx} (80%) rename src/reactviews/pages/{DataTierApplication/dataTierApplicationPage.tsx => DacFxApplication/dacFxApplicationPage.tsx} (72%) rename src/reactviews/pages/{DataTierApplication/dataTierApplicationSelector.ts => DacFxApplication/dacFxApplicationSelector.ts} (60%) create mode 100644 src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx rename src/reactviews/pages/{DataTierApplication => DacFxApplication}/index.tsx (68%) delete mode 100644 src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx rename src/sharedInterfaces/{dataTierApplication.ts => dacFxApplication.ts} (80%) rename test/unit/{dataTierApplicationWebviewController.test.ts => dacFxApplicationWebviewController.test.ts} (97%) diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index f2ad92ee6d..f34fcdb951 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -4047,7 +4047,7 @@ Create a new table in your database, or edit existing tables with the table designer. Once you're done making your changes, click the 'Publish' button to send the changes to your database. - + Data-tier Application diff --git a/package.json b/package.json index a8f311c7f5..37807fa531 100644 --- a/package.json +++ b/package.json @@ -548,7 +548,7 @@ "group": "2_MSSQL_serverDbActions@2" }, { - "command": "mssql.dataTierApplication", + "command": "mssql.dacFxApplication", "when": "view == objectExplorer && viewItem =~ /\\btype=(disconnectedServer|Server|Database)\\b/", "group": "2_MSSQL_serverDbActions@3" }, @@ -1034,8 +1034,8 @@ "category": "MS SQL" }, { - "command": "mssql.dataTierApplication", - "title": "%mssql.dataTierApplication%", + "command": "mssql.dacFxApplication", + "title": "%mssql.dacFxApplication%", "category": "MS SQL", "icon": "$(database)" }, diff --git a/package.nls.json b/package.nls.json index 8b555bd10e..a2f82d368b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -15,7 +15,7 @@ "mssql.scriptDelete": "Script as Drop", "mssql.scriptExecute": "Script as Execute", "mssql.scriptAlter": "Script as Alter", - "mssql.dataTierApplication": "Data-tier Application", + "mssql.dacFxApplication": "Data-tier Application", "mssql.deployDacpac": "Deploy DACPAC", "mssql.extractDacpac": "Extract DACPAC", "mssql.importBacpac": "Import BACPAC", diff --git a/scripts/bundle-reactviews.js b/scripts/bundle-reactviews.js index d4aea98b8f..acdcfafdaf 100644 --- a/scripts/bundle-reactviews.js +++ b/scripts/bundle-reactviews.js @@ -17,7 +17,7 @@ const config = { addFirewallRule: "src/reactviews/pages/AddFirewallRule/index.tsx", connectionDialog: "src/reactviews/pages/ConnectionDialog/index.tsx", connectionGroup: "src/reactviews/pages/ConnectionGroup/index.tsx", - dataTierApplication: "src/reactviews/pages/DataTierApplication/index.tsx", + dacFxApplication: "src/reactviews/pages/DacFxApplication/index.tsx", deployment: "src/reactviews/pages/Deployment/index.tsx", executionPlan: "src/reactviews/pages/ExecutionPlan/index.tsx", tableDesigner: "src/reactviews/pages/TableDesigner/index.tsx", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 1a90f6d981..af5d44f0e5 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -52,7 +52,7 @@ export const cmdNewQuery = "mssql.newQuery"; export const cmdCopilotNewQueryWithConnection = "mssql.copilot.newQueryWithConnection"; export const cmdSchemaCompare = "mssql.schemaCompare"; export const cmdSchemaCompareOpenFromCommandPalette = "mssql.schemaCompareOpenFromCommandPalette"; -export const cmdDataTierApplication = "mssql.dataTierApplication"; +export const cmdDacFxApplication = "mssql.dacFxApplication"; export const cmdDeployDacpac = "mssql.deployDacpac"; export const cmdExtractDacpac = "mssql.extractDacpac"; export const cmdImportBacpac = "mssql.importBacpac"; diff --git a/src/constants/locConstants.ts b/src/constants/locConstants.ts index 8df7a934cc..998fe1b602 100644 --- a/src/constants/locConstants.ts +++ b/src/constants/locConstants.ts @@ -2073,7 +2073,7 @@ export class ConnectionGroup { }; } -export class DataTierApplication { +export class DacFxApplication { public static Title = l10n.t("Data-tier Application"); public static FilePathRequired = l10n.t("File path is required"); public static FileNotFound = l10n.t("File not found"); diff --git a/src/controllers/dataTierApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts similarity index 91% rename from src/controllers/dataTierApplicationWebviewController.ts rename to src/controllers/dacFxApplicationWebviewController.ts index 828f557b69..0498e9213a 100644 --- a/src/controllers/dataTierApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -19,13 +19,13 @@ import { TelemetryViews, TelemetryActions, ActivityStatus } from "../sharedInter import { BrowseInputFileWebviewRequest, BrowseOutputFileWebviewRequest, - CancelDataTierApplicationWebviewNotification, + CancelDacFxApplicationWebviewNotification, ConfirmDeployToExistingWebviewRequest, ConnectionProfile, ConnectToServerWebviewRequest, - DataTierApplicationResult, - DataTierApplicationWebviewState, - DataTierOperationType, + DacFxApplicationResult, + DacFxApplicationWebviewState, + DacFxOperationType, DeployDacpacParams, DeployDacpacWebviewRequest, ExportBacpacParams, @@ -41,18 +41,18 @@ import { ListDatabasesWebviewRequest, ValidateDatabaseNameWebviewRequest, ValidateFilePathWebviewRequest, -} from "../sharedInterfaces/dataTierApplication"; +} from "../sharedInterfaces/dacFxApplication"; import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; /** - * Controller for the Data-tier Application webview - * Manages DACPAC and BACPAC operations (Deploy, Extract, Import, Export) + * Controller for the DacFxApplication webview. + * Manages DACPAC and BACPAC operations (Deploy, Extract, Import, Export) using the Data-tier Application Framework (DacFx). */ -export class DataTierApplicationWebviewController extends ReactWebviewPanelController< - DataTierApplicationWebviewState, +export class DacFxApplicationWebviewController extends ReactWebviewPanelController< + DacFxApplicationWebviewState, void, - DataTierApplicationResult + DacFxApplicationResult > { private _ownerUri: string; @@ -61,11 +61,11 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr vscodeWrapper: VscodeWrapper, private connectionManager: ConnectionManager, private dacFxService: DacFxService, - initialState: DataTierApplicationWebviewState, + initialState: DacFxApplicationWebviewState, ownerUri: string, ) { - super(context, vscodeWrapper, "dataTierApplication", "dataTierApplication", initialState, { - title: LocConstants.DataTierApplication.Title, + super(context, vscodeWrapper, "dacFxApplication", "dacFxApplication", initialState, { + title: LocConstants.DacFxApplication.Title, viewColumn: vscode.ViewColumn.Active, iconPath: { dark: vscode.Uri.joinPath(context.extensionUri, "media", "database_dark.svg"), @@ -126,7 +126,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr databaseName: string; ownerUri: string; shouldNotExist: boolean; - operationType?: DataTierOperationType; + operationType?: DacFxOperationType; }) => { if (!params.ownerUri || params.ownerUri.trim() === "") { this.logger.error("Cannot validate database name: ownerUri is empty"); @@ -179,7 +179,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - openLabel: LocConstants.DataTierApplication.Select, + openLabel: LocConstants.DacFxApplication.Select, filters: { [`${params.fileExtension.toUpperCase()} Files`]: [params.fileExtension], }, @@ -206,7 +206,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr const fileUri = await vscode.window.showSaveDialog({ defaultUri: defaultUri, - saveLabel: LocConstants.DataTierApplication.Save, + saveLabel: LocConstants.DacFxApplication.Save, filters: { [`${params.fileExtension.toUpperCase()} Files`]: [params.fileExtension], }, @@ -223,9 +223,9 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr // Get default output path without showing dialog this.onRequest( GetSuggestedOutputPathWebviewRequest.type, - async (params: { databaseName: string; operationType: DataTierOperationType }) => { + async (params: { databaseName: string; operationType: DacFxOperationType }) => { const fileExtension = - params.operationType === DataTierOperationType.Extract ? "dacpac" : "bacpac"; + params.operationType === DacFxOperationType.Extract ? "dacpac" : "bacpac"; // Format timestamp as yyyy-MM-dd-HH-mm const now = new Date(); @@ -266,18 +266,18 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr // Confirm deploy to existing database request handler this.onRequest(ConfirmDeployToExistingWebviewRequest.type, async () => { const result = await this.vscodeWrapper.showWarningMessageAdvanced( - LocConstants.DataTierApplication.DeployToExistingMessage, + LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, - [LocConstants.DataTierApplication.DeployToExistingConfirm], + [LocConstants.DacFxApplication.DeployToExistingConfirm], ); return { - confirmed: result === LocConstants.DataTierApplication.DeployToExistingConfirm, + confirmed: result === LocConstants.DacFxApplication.DeployToExistingConfirm, }; }); // Cancel operation notification handler - this.onNotification(CancelDataTierApplicationWebviewNotification.type, () => { + this.onNotification(CancelDacFxApplicationWebviewNotification.type, () => { this.dialogResult.resolve(undefined); this.panel.dispose(); }); @@ -286,11 +286,9 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr /** * Handles deploying a DACPAC file to a database */ - private async handleDeployDacpac( - params: DeployDacpacParams, - ): Promise { + private async handleDeployDacpac(params: DeployDacpacParams): Promise { const activity = startActivity( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxDeployDacpac, undefined, { @@ -307,7 +305,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr TaskExecutionMode.execute, ); - const appResult: DataTierApplicationResult = { + const appResult: DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -340,9 +338,9 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr */ private async handleExtractDacpac( params: ExtractDacpacParams, - ): Promise { + ): Promise { const activity = startActivity( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxExtractDacpac, ); @@ -356,7 +354,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr TaskExecutionMode.execute, ); - const appResult: DataTierApplicationResult = { + const appResult: DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -387,11 +385,9 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr /** * Handles importing a BACPAC file to create a new database */ - private async handleImportBacpac( - params: ImportBacpacParams, - ): Promise { + private async handleImportBacpac(params: ImportBacpacParams): Promise { const activity = startActivity( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxImportBacpac, ); @@ -403,7 +399,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr TaskExecutionMode.execute, ); - const appResult: DataTierApplicationResult = { + const appResult: DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -434,11 +430,9 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr /** * Handles exporting a database to a BACPAC file */ - private async handleExportBacpac( - params: ExportBacpacParams, - ): Promise { + private async handleExportBacpac(params: ExportBacpacParams): Promise { const activity = startActivity( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxExportBacpac, ); @@ -450,7 +444,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr TaskExecutionMode.execute, ); - const appResult: DataTierApplicationResult = { + const appResult: DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -488,7 +482,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (!filePath || filePath.trim() === "") { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.FilePathRequired, + errorMessage: LocConstants.DacFxApplication.FilePathRequired, }; } @@ -497,7 +491,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (shouldExist && !fileFound) { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.FileNotFound, + errorMessage: LocConstants.DacFxApplication.FileNotFound, }; } @@ -505,7 +499,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (extension !== ".dacpac" && extension !== ".bacpac") { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.InvalidFileExtension, + errorMessage: LocConstants.DacFxApplication.InvalidFileExtension, }; } @@ -515,7 +509,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (!fs.existsSync(directory)) { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.DirectoryNotFound, + errorMessage: LocConstants.DacFxApplication.DirectoryNotFound, }; } @@ -524,7 +518,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr // This is just a warning - the operation can continue with user confirmation return { isValid: true, - errorMessage: LocConstants.DataTierApplication.FileAlreadyExists, + errorMessage: LocConstants.DacFxApplication.FileAlreadyExists, }; } } @@ -906,12 +900,12 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr databaseName: string, ownerUri: string, shouldNotExist: boolean, - operationType?: DataTierOperationType, + operationType?: DacFxOperationType, ): Promise<{ isValid: boolean; errorMessage?: string }> { if (!databaseName || databaseName.trim() === "") { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.DatabaseNameRequired, + errorMessage: LocConstants.DacFxApplication.DatabaseNameRequired, }; } @@ -920,7 +914,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (invalidChars.test(databaseName)) { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.InvalidDatabaseName, + errorMessage: LocConstants.DacFxApplication.InvalidDatabaseName, }; } @@ -928,7 +922,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (databaseName.length > 128) { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.DatabaseNameTooLong, + errorMessage: LocConstants.DacFxApplication.DatabaseNameTooLong, }; } @@ -946,10 +940,10 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr // This ensures confirmation dialog is shown in both cases: // 1. User selected "New Database" but database already exists (shouldNotExist=true) // 2. User selected "Existing Database" and selected existing database (shouldNotExist=false) - if (operationType === DataTierOperationType.Deploy && exists) { + if (operationType === DacFxOperationType.Deploy && exists) { return { isValid: true, // Allow the operation but with a warning - errorMessage: LocConstants.DataTierApplication.DatabaseAlreadyExists, + errorMessage: LocConstants.DacFxApplication.DatabaseAlreadyExists, }; } @@ -957,7 +951,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (shouldNotExist && exists) { return { isValid: true, // Allow the operation but with a warning - errorMessage: LocConstants.DataTierApplication.DatabaseAlreadyExists, + errorMessage: LocConstants.DacFxApplication.DatabaseAlreadyExists, }; } @@ -965,7 +959,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr if (!shouldNotExist && !exists) { return { isValid: false, - errorMessage: LocConstants.DataTierApplication.DatabaseNotFound, + errorMessage: LocConstants.DacFxApplication.DatabaseNotFound, }; } @@ -974,7 +968,7 @@ export class DataTierApplicationWebviewController extends ReactWebviewPanelContr const errorMessage = error instanceof Error ? `Failed to validate database name: ${error.message}` - : LocConstants.DataTierApplication.ValidationFailed; + : LocConstants.DacFxApplication.ValidationFailed; this.logger.error(errorMessage); return { isValid: false, diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index b2b9623a8f..2418bcec1e 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -47,11 +47,11 @@ import { ActivityStatus, TelemetryActions, TelemetryViews } from "../sharedInter import { TableDesignerService } from "../services/tableDesignerService"; import { TableDesignerWebviewController } from "../tableDesigner/tableDesignerWebviewController"; import { ConnectionDialogWebviewController } from "../connectionconfig/connectionDialogWebviewController"; -import { DataTierApplicationWebviewController } from "./dataTierApplicationWebviewController"; +import { DacFxApplicationWebviewController } from "./dacFxApplicationWebviewController"; import { - DataTierApplicationWebviewState, - DataTierOperationType, -} from "../sharedInterfaces/dataTierApplication"; + DacFxApplicationWebviewState, + DacFxOperationType, +} from "../sharedInterfaces/dacFxApplication"; import { ObjectExplorerFilter } from "../objectExplorer/objectExplorerFilter"; import { DatabaseObjectSearchService, @@ -1790,7 +1790,7 @@ export default class MainController implements vscode.Disposable { // Data-tier Application - Main command this._context.subscriptions.push( vscode.commands.registerCommand( - Constants.cmdDataTierApplication, + Constants.cmdDacFxApplication, async (node?: TreeNodeInfo) => { const connectionProfile = node?.connectionProfile; const ownerUri = connectionProfile @@ -1803,15 +1803,15 @@ export default class MainController implements vscode.Disposable { `${connectionProfile.server}_${connectionProfile.database || ""}` : undefined; - const initialState: DataTierApplicationWebviewState = { + const initialState: DacFxApplicationWebviewState = { ownerUri, serverName, databaseName, selectedProfileId: profileId, - operationType: DataTierOperationType.Deploy, + operationType: DacFxOperationType.Deploy, }; - const controller = new DataTierApplicationWebviewController( + const controller = new DacFxApplicationWebviewController( this._context, this._vscodeWrapper, this._connectionMgr, @@ -1840,15 +1840,15 @@ export default class MainController implements vscode.Disposable { `${connectionProfile.server}_${connectionProfile.database || ""}` : undefined; - const initialState: DataTierApplicationWebviewState = { + const initialState: DacFxApplicationWebviewState = { ownerUri, serverName, databaseName, selectedProfileId: profileId, - operationType: DataTierOperationType.Deploy, + operationType: DacFxOperationType.Deploy, }; - const controller = new DataTierApplicationWebviewController( + const controller = new DacFxApplicationWebviewController( this._context, this._vscodeWrapper, this._connectionMgr, @@ -1877,15 +1877,15 @@ export default class MainController implements vscode.Disposable { `${connectionProfile.server}_${connectionProfile.database || ""}` : undefined; - const initialState: DataTierApplicationWebviewState = { + const initialState: DacFxApplicationWebviewState = { ownerUri, serverName, databaseName, selectedProfileId: profileId, - operationType: DataTierOperationType.Extract, + operationType: DacFxOperationType.Extract, }; - const controller = new DataTierApplicationWebviewController( + const controller = new DacFxApplicationWebviewController( this._context, this._vscodeWrapper, this._connectionMgr, @@ -1914,15 +1914,15 @@ export default class MainController implements vscode.Disposable { `${connectionProfile.server}_${connectionProfile.database || ""}` : undefined; - const initialState: DataTierApplicationWebviewState = { + const initialState: DacFxApplicationWebviewState = { ownerUri, serverName, databaseName, selectedProfileId: profileId, - operationType: DataTierOperationType.Import, + operationType: DacFxOperationType.Import, }; - const controller = new DataTierApplicationWebviewController( + const controller = new DacFxApplicationWebviewController( this._context, this._vscodeWrapper, this._connectionMgr, @@ -1951,15 +1951,15 @@ export default class MainController implements vscode.Disposable { `${connectionProfile.server}_${connectionProfile.database || ""}` : undefined; - const initialState: DataTierApplicationWebviewState = { + const initialState: DacFxApplicationWebviewState = { ownerUri, serverName, databaseName, selectedProfileId: profileId, - operationType: DataTierOperationType.Export, + operationType: DacFxOperationType.Export, }; - const controller = new DataTierApplicationWebviewController( + const controller = new DacFxApplicationWebviewController( this._context, this._vscodeWrapper, this._connectionMgr, diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index 258ea17307..07efef28ef 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -852,7 +852,7 @@ export class LocConstants { selectSource: l10n.t("Select Source"), selectTarget: l10n.t("Select Target"), close: l10n.t("Close"), - dataTierApplicationFile: l10n.t("Data-tier Application File (.dacpac)"), + dacFxApplicationFile: l10n.t("Data-tier Application File (.dacpac)"), databaseProject: l10n.t("Database Project"), ok: l10n.t("OK"), cancel: l10n.t("Cancel"), @@ -1087,7 +1087,7 @@ export class LocConstants { }; } - public get dataTierApplication() { + public get dacFxApplication() { return { title: l10n.t("Data-tier Application"), subtitle: l10n.t( diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplication.css b/src/reactviews/pages/DacFxApplication/dacFxApplication.css similarity index 100% rename from src/reactviews/pages/DataTierApplication/dataTierApplication.css rename to src/reactviews/pages/DacFxApplication/dacFxApplication.css diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx similarity index 80% rename from src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx rename to src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index eddfbe58a0..d082512fe6 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -23,7 +23,7 @@ import { BrowseOutputFileWebviewRequest, ConnectionProfile, ConnectToServerWebviewRequest, - DataTierOperationType, + DacFxOperationType, DeployDacpacWebviewRequest, ExtractDacpacWebviewRequest, ImportBacpacWebviewRequest, @@ -34,11 +34,11 @@ import { ValidateFilePathWebviewRequest, ListDatabasesWebviewRequest, ValidateDatabaseNameWebviewRequest, - CancelDataTierApplicationWebviewNotification, + CancelDacFxApplicationWebviewNotification, ConfirmDeployToExistingWebviewRequest, -} from "../../../sharedInterfaces/dataTierApplication"; -import { DataTierApplicationContext } from "./dataTierApplicationStateProvider"; -import { useDataTierApplicationSelector } from "./dataTierApplicationSelector"; +} from "../../../sharedInterfaces/dacFxApplication"; +import { DacFxApplicationContext } from "./dacFxApplicationStateProvider"; +import { useDacFxApplicationSelector } from "./dacFxApplicationSelector"; import { locConstants } from "../../common/locConstants"; /** @@ -119,22 +119,22 @@ const useStyles = makeStyles({ }, }); -export const DataTierApplicationForm = () => { +export const DacFxApplicationForm = () => { const classes = useStyles(); - const context = useContext(DataTierApplicationContext); + const context = useContext(DacFxApplicationContext); // State from the controller - const initialOperationType = useDataTierApplicationSelector((state) => state.operationType); - const initialOwnerUri = useDataTierApplicationSelector((state) => state.ownerUri); - const initialServerName = useDataTierApplicationSelector((state) => state.serverName); - const initialDatabaseName = useDataTierApplicationSelector((state) => state.databaseName); - const initialSelectedProfileId = useDataTierApplicationSelector( + const initialOperationType = useDacFxApplicationSelector((state) => state.operationType); + const initialOwnerUri = useDacFxApplicationSelector((state) => state.ownerUri); + const initialServerName = useDacFxApplicationSelector((state) => state.serverName); + const initialDatabaseName = useDacFxApplicationSelector((state) => state.databaseName); + const initialSelectedProfileId = useDacFxApplicationSelector( (state) => state.selectedProfileId, ); // Local state - const [operationType, setOperationType] = useState( - initialOperationType || DataTierOperationType.Deploy, + const [operationType, setOperationType] = useState( + initialOperationType || DacFxOperationType.Deploy, ); const [filePath, setFilePath] = useState(""); const [databaseName, setDatabaseName] = useState(initialDatabaseName || ""); @@ -163,7 +163,7 @@ export const DataTierApplicationForm = () => { return () => { if (isConnecting || isOperationInProgress) { void context?.extensionRpc?.sendNotification( - CancelDataTierApplicationWebviewNotification.type, + CancelDacFxApplicationWebviewNotification.type, ); } }; @@ -173,9 +173,9 @@ export const DataTierApplicationForm = () => { useEffect(() => { if ( ownerUri && - (operationType === DataTierOperationType.Deploy || - operationType === DataTierOperationType.Extract || - operationType === DataTierOperationType.Export) + (operationType === DacFxOperationType.Deploy || + operationType === DacFxOperationType.Extract || + operationType === DacFxOperationType.Export) ) { void loadDatabases(); } @@ -186,8 +186,8 @@ export const DataTierApplicationForm = () => { const updateSuggestedPath = async () => { if ( databaseName && - (operationType === DataTierOperationType.Extract || - operationType === DataTierOperationType.Export) && + (operationType === DacFxOperationType.Extract || + operationType === DacFxOperationType.Export) && context?.extensionRpc ) { // Get the suggested full path from the controller @@ -240,7 +240,7 @@ export const DataTierApplicationForm = () => { setValidationMessages((prev) => ({ ...prev, connection: { - message: `${locConstants.dataTierApplication.connectionFailed}: ${result.errorMessage}`, + message: `${locConstants.dacFxApplication.connectionFailed}: ${result.errorMessage}`, severity: "error", }, })); @@ -251,7 +251,7 @@ export const DataTierApplicationForm = () => { const errorMsg = error instanceof Error ? error.message : String(error); setValidationMessages({ connection: { - message: `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, + message: `${locConstants.dacFxApplication.connectionFailed}: ${errorMsg}`, severity: "error", }, }); @@ -305,7 +305,7 @@ export const DataTierApplicationForm = () => { ); // Show error message to user const errorMsg = - result?.errorMessage || locConstants.dataTierApplication.connectionFailed; + result?.errorMessage || locConstants.dacFxApplication.connectionFailed; setValidationMessages({ connection: { message: errorMsg, @@ -336,7 +336,7 @@ export const DataTierApplicationForm = () => { ); // Show error message to user const errorMsg = - result?.errorMessage || locConstants.dataTierApplication.connectionFailed; + result?.errorMessage || locConstants.dacFxApplication.connectionFailed; setValidationMessages({ connection: { message: errorMsg, @@ -349,7 +349,7 @@ export const DataTierApplicationForm = () => { const errorMsg = error instanceof Error ? error.message : String(error); setValidationMessages({ connection: { - message: `${locConstants.dataTierApplication.connectionFailed}: ${errorMsg}`, + message: `${locConstants.dacFxApplication.connectionFailed}: ${errorMsg}`, severity: "error", }, }); @@ -372,7 +372,7 @@ export const DataTierApplicationForm = () => { setValidationMessages((prev) => ({ ...prev, database: { - message: `${locConstants.dataTierApplication.failedToLoadDatabases}: ${errorMsg}`, + message: `${locConstants.dacFxApplication.failedToLoadDatabases}: ${errorMsg}`, severity: "error", }, })); @@ -384,7 +384,7 @@ export const DataTierApplicationForm = () => { setValidationMessages((prev) => ({ ...prev, filePath: { - message: locConstants.dataTierApplication.filePathRequired, + message: locConstants.dacFxApplication.filePathRequired, severity: "error", }, })); @@ -401,8 +401,7 @@ export const DataTierApplicationForm = () => { setValidationMessages((prev) => ({ ...prev, filePath: { - message: - result?.errorMessage || locConstants.dataTierApplication.invalidFile, + message: result?.errorMessage || locConstants.dacFxApplication.invalidFile, severity: "error", }, })); @@ -430,7 +429,7 @@ export const DataTierApplicationForm = () => { const errorMessage = error instanceof Error ? error.message - : locConstants.dataTierApplication.validationFailed; + : locConstants.dacFxApplication.validationFailed; setValidationMessages((prev) => ({ ...prev, filePath: { @@ -450,7 +449,7 @@ export const DataTierApplicationForm = () => { setValidationMessages((prev) => ({ ...prev, databaseName: { - message: locConstants.dataTierApplication.databaseNameRequired, + message: locConstants.dacFxApplication.databaseNameRequired, severity: "error", }, })); @@ -473,8 +472,7 @@ export const DataTierApplicationForm = () => { ...prev, databaseName: { message: - result?.errorMessage || - locConstants.dataTierApplication.invalidDatabase, + result?.errorMessage || locConstants.dacFxApplication.invalidDatabase, severity: "error", }, })); @@ -493,8 +491,8 @@ export const DataTierApplicationForm = () => { // 1. User checked "New Database" but database already exists (shouldNotExist=true) // 2. User unchecked "New Database" to deploy to existing (shouldNotExist=false) if ( - operationType === DataTierOperationType.Deploy && - result.errorMessage === locConstants.dataTierApplication.databaseAlreadyExists + operationType === DacFxOperationType.Deploy && + result.errorMessage === locConstants.dacFxApplication.databaseAlreadyExists ) { const confirmResult = await context?.extensionRpc?.sendRequest( ConfirmDeployToExistingWebviewRequest.type, @@ -509,7 +507,7 @@ export const DataTierApplicationForm = () => { const errorMessage = error instanceof Error ? error.message - : locConstants.dataTierApplication.validationFailed; + : locConstants.dacFxApplication.validationFailed; setValidationMessages((prev) => ({ ...prev, databaseName: { @@ -537,7 +535,7 @@ export const DataTierApplicationForm = () => { let result; switch (operationType) { - case DataTierOperationType.Deploy: + case DacFxOperationType.Deploy: if ( !(await validateFilePath(filePath, true)) || !(await validateDatabaseName(databaseName, isNewDatabase)) @@ -556,7 +554,7 @@ export const DataTierApplicationForm = () => { ); break; - case DataTierOperationType.Extract: + case DacFxOperationType.Extract: if ( !(await validateFilePath(filePath, false)) || !(await validateDatabaseName(databaseName, false)) @@ -576,7 +574,7 @@ export const DataTierApplicationForm = () => { ); break; - case DataTierOperationType.Import: + case DacFxOperationType.Import: if ( !(await validateFilePath(filePath, true)) || !(await validateDatabaseName(databaseName, true)) @@ -594,7 +592,7 @@ export const DataTierApplicationForm = () => { ); break; - case DataTierOperationType.Export: + case DacFxOperationType.Export: if ( !(await validateFilePath(filePath, false)) || !(await validateDatabaseName(databaseName, false)) @@ -618,7 +616,7 @@ export const DataTierApplicationForm = () => { clearForm(); } else { console.error( - result?.errorMessage || locConstants.dataTierApplication.operationFailed, + result?.errorMessage || locConstants.dacFxApplication.operationFailed, ); setIsOperationInProgress(false); } @@ -626,7 +624,7 @@ export const DataTierApplicationForm = () => { console.error( error instanceof Error ? error.message - : locConstants.dataTierApplication.unexpectedError, + : locConstants.dacFxApplication.unexpectedError, ); setIsOperationInProgress(false); } @@ -634,8 +632,8 @@ export const DataTierApplicationForm = () => { const handleBrowseFile = async () => { const fileExtension = - operationType === DataTierOperationType.Deploy || - operationType === DataTierOperationType.Extract + operationType === DacFxOperationType.Deploy || + operationType === DacFxOperationType.Extract ? "dacpac" : "bacpac"; @@ -691,8 +689,8 @@ export const DataTierApplicationForm = () => { if ( requiresInputFile && context?.extensionRpc && - (operationType === DataTierOperationType.Deploy || - operationType === DataTierOperationType.Import) + (operationType === DacFxOperationType.Deploy || + operationType === DacFxOperationType.Import) ) { const nameResult = await context.extensionRpc.sendRequest( GetSuggestedDatabaseNameWebviewRequest.type, @@ -714,7 +712,7 @@ export const DataTierApplicationForm = () => { const handleCancel = async () => { await context?.extensionRpc?.sendNotification( - CancelDataTierApplicationWebviewNotification.type, + CancelDacFxApplicationWebviewNotification.type, ); }; @@ -729,14 +727,12 @@ export const DataTierApplicationForm = () => { }; const requiresInputFile = - operationType === DataTierOperationType.Deploy || - operationType === DataTierOperationType.Import; - const showDatabaseTarget = operationType === DataTierOperationType.Deploy; + operationType === DacFxOperationType.Deploy || operationType === DacFxOperationType.Import; + const showDatabaseTarget = operationType === DacFxOperationType.Deploy; const showDatabaseSource = - operationType === DataTierOperationType.Extract || - operationType === DataTierOperationType.Export; - const showNewDatabase = operationType === DataTierOperationType.Import; - const showApplicationInfo = operationType === DataTierOperationType.Extract; + operationType === DacFxOperationType.Extract || operationType === DacFxOperationType.Export; + const showNewDatabase = operationType === DacFxOperationType.Import; + const showApplicationInfo = operationType === DacFxOperationType.Extract; async function handleFilePathChange(value: string): Promise { setFilePath(value); @@ -751,18 +747,18 @@ export const DataTierApplicationForm = () => {
-
{locConstants.dataTierApplication.title}
+
{locConstants.dacFxApplication.title}
- {locConstants.dataTierApplication.subtitle} + {locConstants.dacFxApplication.subtitle}
- + { - setOperationType(data.value as DataTierOperationType); + setOperationType(data.value as DacFxOperationType); setValidationMessages({}); // Reset file path when switching operation types // Import/Deploy need empty (browse for existing file) @@ -770,46 +766,46 @@ export const DataTierApplicationForm = () => { setFilePath(""); }} disabled={isOperationInProgress} - aria-label={locConstants.dataTierApplication.operationLabel}> + aria-label={locConstants.dacFxApplication.operationLabel}> @@ -817,7 +813,7 @@ export const DataTierApplicationForm = () => {
{ {isConnecting ? ( ) : ( { disabled={ isOperationInProgress || availableConnections.length === 0 } - aria-label={locConstants.dataTierApplication.serverLabel}> + aria-label={locConstants.dacFxApplication.serverLabel}> {availableConnections.length === 0 ? ( ) : ( availableConnections.map((conn) => ( @@ -870,8 +866,8 @@ export const DataTierApplicationForm = () => { { onChange={(_, data) => handleFilePathChange(data.value)} placeholder={ requiresInputFile - ? locConstants.dataTierApplication.selectPackageFile - : locConstants.dataTierApplication.selectOutputFile + ? locConstants.dacFxApplication.selectPackageFile + : locConstants.dacFxApplication.selectOutputFile } disabled={isOperationInProgress} aria-label={ requiresInputFile - ? locConstants.dataTierApplication.packageFileLabel - : locConstants.dataTierApplication.outputFileLabel + ? locConstants.dacFxApplication.packageFileLabel + : locConstants.dacFxApplication.outputFileLabel } />
@@ -913,29 +909,29 @@ export const DataTierApplicationForm = () => { {showDatabaseTarget && (
- + setIsNewDatabase(data.value === "new")} className={classes.radioGroup} - aria-label={locConstants.dataTierApplication.targetDatabaseLabel}> + aria-label={locConstants.dacFxApplication.targetDatabaseLabel}> {isNewDatabase ? ( { setDatabaseName(data.value)} - placeholder={locConstants.dataTierApplication.enterDatabaseName} + placeholder={locConstants.dacFxApplication.enterDatabaseName} disabled={isOperationInProgress} - aria-label={locConstants.dataTierApplication.databaseNameLabel} + aria-label={locConstants.dacFxApplication.databaseNameLabel} /> ) : ( { : "none" }> setDatabaseName(data.optionText || "") } disabled={isOperationInProgress || !ownerUri} - aria-label={locConstants.dataTierApplication.databaseNameLabel}> + aria-label={locConstants.dacFxApplication.databaseNameLabel}> {availableDatabases.map((db) => (
diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationPage.tsx similarity index 72% rename from src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx rename to src/reactviews/pages/DacFxApplication/dacFxApplicationPage.tsx index 40ce38e90e..48c0f64109 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationPage.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationPage.tsx @@ -5,12 +5,12 @@ import { useContext } from "react"; import { Spinner } from "@fluentui/react-components"; -import { DataTierApplicationContext } from "./dataTierApplicationStateProvider"; -import { DataTierApplicationForm } from "./dataTierApplicationForm"; +import { DacFxApplicationContext } from "./dacFxApplicationStateProvider"; +import { DacFxApplicationForm } from "./dacFxApplicationForm"; import { locConstants } from "../../common/locConstants"; -export const DataTierApplicationPage = () => { - const context = useContext(DataTierApplicationContext); +export const DacFxApplicationPage = () => { + const context = useContext(DacFxApplicationContext); if (!context) { return ( @@ -27,10 +27,10 @@ export const DataTierApplicationPage = () => { alignItems: "center", justifyContent: "center", }}> - +
); } - return ; + return ; }; diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.ts b/src/reactviews/pages/DacFxApplication/dacFxApplicationSelector.ts similarity index 60% rename from src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.ts rename to src/reactviews/pages/DacFxApplication/dacFxApplicationSelector.ts index 8808b0f535..94b4afda04 100644 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationSelector.ts +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationSelector.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DataTierApplicationWebviewState } from "../../../sharedInterfaces/dataTierApplication"; +import { DacFxApplicationWebviewState } from "../../../sharedInterfaces/dacFxApplication"; import { useVscodeSelector } from "../../common/useVscodeSelector"; -export function useDataTierApplicationSelector( - selector: (state: DataTierApplicationWebviewState) => T, +export function useDacFxApplicationSelector( + selector: (state: DacFxApplicationWebviewState) => T, equals?: (a: T, b: T) => boolean, ) { - return useVscodeSelector(selector, equals); + return useVscodeSelector(selector, equals); } diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx new file mode 100644 index 0000000000..5c4237bd65 --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React, { createContext, ReactNode } from "react"; +import { DacFxApplicationWebviewState } from "../../../sharedInterfaces/dacFxApplication"; +import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2"; +import { WebviewRpc } from "../../common/rpc"; + +export interface DacFxApplicationReactProvider { + extensionRpc: WebviewRpc; +} + +export const DacFxApplicationContext = createContext( + undefined, +); + +interface DacFxApplicationProviderProps { + children: ReactNode; +} + +const DacFxApplicationStateProvider: React.FC = ({ children }) => { + const { extensionRpc } = useVscodeWebview2(); + return ( + + {children} + + ); +}; + +export { DacFxApplicationStateProvider }; diff --git a/src/reactviews/pages/DataTierApplication/index.tsx b/src/reactviews/pages/DacFxApplication/index.tsx similarity index 68% rename from src/reactviews/pages/DataTierApplication/index.tsx rename to src/reactviews/pages/DacFxApplication/index.tsx index 96d4544cb0..afcc19aeba 100644 --- a/src/reactviews/pages/DataTierApplication/index.tsx +++ b/src/reactviews/pages/DacFxApplication/index.tsx @@ -6,13 +6,13 @@ import ReactDOM from "react-dom/client"; import "../../index.css"; import { VscodeWebviewProvider2 } from "../../common/vscodeWebviewProvider2"; -import { DataTierApplicationStateProvider } from "./dataTierApplicationStateProvider"; -import { DataTierApplicationPage } from "./dataTierApplicationPage"; +import { DacFxApplicationStateProvider } from "./dacFxApplicationStateProvider"; +import { DacFxApplicationPage } from "./dacFxApplicationPage"; ReactDOM.createRoot(document.getElementById("root")!).render( - - - + + + , ); diff --git a/src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx b/src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx deleted file mode 100644 index bcec7dc4f0..0000000000 --- a/src/reactviews/pages/DataTierApplication/dataTierApplicationStateProvider.tsx +++ /dev/null @@ -1,34 +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 React, { createContext, ReactNode } from "react"; -import { DataTierApplicationWebviewState } from "../../../sharedInterfaces/dataTierApplication"; -import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2"; -import { WebviewRpc } from "../../common/rpc"; - -export interface DataTierApplicationReactProvider { - extensionRpc: WebviewRpc; -} - -export const DataTierApplicationContext = createContext< - DataTierApplicationReactProvider | undefined ->(undefined); - -interface DataTierApplicationProviderProps { - children: ReactNode; -} - -const DataTierApplicationStateProvider: React.FC = ({ - children, -}) => { - const { extensionRpc } = useVscodeWebview2(); - return ( - - {children} - - ); -}; - -export { DataTierApplicationStateProvider }; diff --git a/src/reactviews/pages/SchemaCompare/components/SchemaSelectorDrawer.tsx b/src/reactviews/pages/SchemaCompare/components/SchemaSelectorDrawer.tsx index 181419a314..06a132e282 100644 --- a/src/reactviews/pages/SchemaCompare/components/SchemaSelectorDrawer.tsx +++ b/src/reactviews/pages/SchemaCompare/components/SchemaSelectorDrawer.tsx @@ -282,7 +282,7 @@ const SchemaSelectorDrawer = (props: Props) => { value={schemaType} onChange={(_, data) => handleSchemaTypeChange(data.value)}> - + {isSqlProjExtensionInstalled && ( )} diff --git a/src/sharedInterfaces/dataTierApplication.ts b/src/sharedInterfaces/dacFxApplication.ts similarity index 80% rename from src/sharedInterfaces/dataTierApplication.ts rename to src/sharedInterfaces/dacFxApplication.ts index b9e0f0d592..c0d948c1b4 100644 --- a/src/sharedInterfaces/dataTierApplication.ts +++ b/src/sharedInterfaces/dacFxApplication.ts @@ -6,9 +6,9 @@ import { NotificationType, RequestType } from "vscode-jsonrpc/browser"; /** - * The type of Data-tier Application operation to perform + * The type of DacFx Application operation to perform */ -export enum DataTierOperationType { +export enum DacFxOperationType { Deploy = "deploy", Extract = "extract", Import = "import", @@ -50,13 +50,13 @@ export interface ConnectionProfile { } /** - * The state of the Data-tier Application webview + * The state of the DacFx Application webview */ -export interface DataTierApplicationWebviewState { +export interface DacFxApplicationWebviewState { /** * The currently selected operation type */ - operationType: DataTierOperationType; + operationType: DacFxOperationType; /** * The selected DACPAC/BACPAC file path */ @@ -151,9 +151,9 @@ export interface ExportBacpacParams { } /** - * Result from a Data-tier Application operation + * Result from a DacFx Application operation */ -export interface DataTierApplicationResult { +export interface DacFxApplicationResult { success: boolean; errorMessage?: string; operationId?: string; @@ -163,8 +163,8 @@ export interface DataTierApplicationResult { * Request to deploy a DACPAC from the webview */ export namespace DeployDacpacWebviewRequest { - export const type = new RequestType( - "dataTierApplication/deployDacpac", + export const type = new RequestType( + "dacFxApplication/deployDacpac", ); } @@ -172,8 +172,8 @@ export namespace DeployDacpacWebviewRequest { * Request to extract a DACPAC from the webview */ export namespace ExtractDacpacWebviewRequest { - export const type = new RequestType( - "dataTierApplication/extractDacpac", + export const type = new RequestType( + "dacFxApplication/extractDacpac", ); } @@ -181,8 +181,8 @@ export namespace ExtractDacpacWebviewRequest { * Request to import a BACPAC from the webview */ export namespace ImportBacpacWebviewRequest { - export const type = new RequestType( - "dataTierApplication/importBacpac", + export const type = new RequestType( + "dacFxApplication/importBacpac", ); } @@ -190,8 +190,8 @@ export namespace ImportBacpacWebviewRequest { * Request to export a BACPAC from the webview */ export namespace ExportBacpacWebviewRequest { - export const type = new RequestType( - "dataTierApplication/exportBacpac", + export const type = new RequestType( + "dacFxApplication/exportBacpac", ); } @@ -203,7 +203,7 @@ export namespace ValidateFilePathWebviewRequest { { filePath: string; shouldExist: boolean }, { isValid: boolean; errorMessage?: string }, void - >("dataTierApplication/validateFilePath"); + >("dacFxApplication/validateFilePath"); } /** @@ -211,7 +211,7 @@ export namespace ValidateFilePathWebviewRequest { */ export namespace ListDatabasesWebviewRequest { export const type = new RequestType<{ ownerUri: string }, { databases: string[] }, void>( - "dataTierApplication/listDatabases", + "dacFxApplication/listDatabases", ); } @@ -224,11 +224,11 @@ export namespace ValidateDatabaseNameWebviewRequest { databaseName: string; ownerUri: string; shouldNotExist: boolean; - operationType?: DataTierOperationType; + operationType?: DacFxOperationType; }, { isValid: boolean; errorMessage?: string }, void - >("dataTierApplication/validateDatabaseName"); + >("dacFxApplication/validateDatabaseName"); } /** @@ -236,7 +236,7 @@ export namespace ValidateDatabaseNameWebviewRequest { */ export namespace ListConnectionsWebviewRequest { export const type = new RequestType( - "dataTierApplication/listConnections", + "dacFxApplication/listConnections", ); } @@ -260,7 +260,7 @@ export namespace InitializeConnectionWebviewRequest { errorMessage?: string; }, void - >("dataTierApplication/initializeConnection"); + >("dacFxApplication/initializeConnection"); } /** @@ -271,22 +271,22 @@ export namespace ConnectToServerWebviewRequest { { profileId: string }, { ownerUri: string; isConnected: boolean; errorMessage?: string }, void - >("dataTierApplication/connectToServer"); + >("dacFxApplication/connectToServer"); } /** * Notification sent from the webview to cancel the operation */ -export namespace CancelDataTierApplicationWebviewNotification { - export const type = new NotificationType("dataTierApplication/cancel"); +export namespace CancelDacFxApplicationWebviewNotification { + export const type = new NotificationType("dacFxApplication/cancel"); } /** * Notification sent to the webview to update progress */ -export namespace DataTierApplicationProgressNotification { +export namespace DacFxApplicationProgressNotification { export const type = new NotificationType<{ message: string; percentage?: number }>( - "dataTierApplication/progress", + "dacFxApplication/progress", ); } @@ -295,7 +295,7 @@ export namespace DataTierApplicationProgressNotification { */ export namespace BrowseInputFileWebviewRequest { export const type = new RequestType<{ fileExtension: string }, { filePath?: string }, void>( - "dataTierApplication/browseInputFile", + "dacFxApplication/browseInputFile", ); } @@ -307,7 +307,7 @@ export namespace BrowseOutputFileWebviewRequest { { fileExtension: string; defaultFileName?: string }, { filePath?: string }, void - >("dataTierApplication/browseOutputFile"); + >("dacFxApplication/browseOutputFile"); } /** @@ -316,10 +316,10 @@ export namespace BrowseOutputFileWebviewRequest { */ export namespace GetSuggestedOutputPathWebviewRequest { export const type = new RequestType< - { databaseName: string; operationType: DataTierOperationType }, + { databaseName: string; operationType: DacFxOperationType }, { fullPath: string }, void - >("dataTierApplication/getSuggestedOutputPath"); + >("dacFxApplication/getSuggestedOutputPath"); } /** @@ -328,7 +328,7 @@ export namespace GetSuggestedOutputPathWebviewRequest { */ export namespace GetSuggestedDatabaseNameWebviewRequest { export const type = new RequestType<{ filePath: string }, { databaseName: string }, void>( - "dataTierApplication/getSuggestedDatabaseName", + "dacFxApplication/getSuggestedDatabaseName", ); } @@ -337,6 +337,6 @@ export namespace GetSuggestedDatabaseNameWebviewRequest { */ export namespace ConfirmDeployToExistingWebviewRequest { export const type = new RequestType( - "dataTierApplication/confirmDeployToExisting", + "dacFxApplication/confirmDeployToExisting", ); } diff --git a/src/sharedInterfaces/telemetry.ts b/src/sharedInterfaces/telemetry.ts index 506ba44610..2dffc1a0ef 100644 --- a/src/sharedInterfaces/telemetry.ts +++ b/src/sharedInterfaces/telemetry.ts @@ -32,7 +32,7 @@ export enum TelemetryViews { Connection = "Connection", Credential = "Credential", ConnectionManager = "ConnectionManager", - DataTierApplication = "DataTierApplication", + DacFxApplication = "DacFxApplication", } export enum TelemetryActions { diff --git a/test/unit/dataTierApplicationWebviewController.test.ts b/test/unit/dacFxApplicationWebviewController.test.ts similarity index 97% rename from test/unit/dataTierApplicationWebviewController.test.ts rename to test/unit/dacFxApplicationWebviewController.test.ts index 30fe9fa9a8..88cbf29395 100644 --- a/test/unit/dataTierApplicationWebviewController.test.ts +++ b/test/unit/dacFxApplicationWebviewController.test.ts @@ -9,15 +9,15 @@ import sinonChai from "sinon-chai"; import * as chai from "chai"; import { expect } from "chai"; import * as jsonRpc from "vscode-jsonrpc/node"; -import { DataTierApplicationWebviewController } from "../../src/controllers/dataTierApplicationWebviewController"; +import { DacFxApplicationWebviewController } from "../../src/controllers/dacFxApplicationWebviewController"; import ConnectionManager from "../../src/controllers/connectionManager"; import { DacFxService } from "../../src/services/dacFxService"; import { - CancelDataTierApplicationWebviewNotification, + CancelDacFxApplicationWebviewNotification, ConfirmDeployToExistingWebviewRequest, ConnectToServerWebviewRequest, - DataTierApplicationResult, - DataTierOperationType, + DacFxApplicationResult, + DacFxOperationType, DeployDacpacWebviewRequest, ExportBacpacWebviewRequest, ExtractDacpacWebviewRequest, @@ -31,7 +31,7 @@ import { ValidateFilePathWebviewRequest, BrowseInputFileWebviewRequest, BrowseOutputFileWebviewRequest, -} from "../../src/sharedInterfaces/dataTierApplication"; +} from "../../src/sharedInterfaces/dacFxApplication"; import * as LocConstants from "../../src/constants/locConstants"; import { stubTelemetry, @@ -57,7 +57,7 @@ import { chai.use(sinonChai); -suite("DataTierApplicationWebviewController", () => { +suite("DacFxApplicationWebviewController", () => { let sandbox: sinon.SinonSandbox; let mockContext: vscode.ExtensionContext; let vscodeWrapperStub: sinon.SinonStubbedInstance; @@ -70,12 +70,12 @@ suite("DataTierApplicationWebviewController", () => { let connectionStub: jsonRpc.MessageConnection; let createWebviewPanelStub: sinon.SinonStub; let panelStub: vscode.WebviewPanel; - let controller: DataTierApplicationWebviewController; + let controller: DacFxApplicationWebviewController; let fsExistsSyncStub: sinon.SinonStub; const ownerUri = "test-connection-uri"; const initialState = { - operationType: DataTierOperationType.Deploy, + operationType: DacFxOperationType.Deploy, serverName: "test-server", }; @@ -124,8 +124,8 @@ suite("DataTierApplicationWebviewController", () => { sandbox.restore(); }); - function createController(): DataTierApplicationWebviewController { - controller = new DataTierApplicationWebviewController( + function createController(): DacFxApplicationWebviewController { + controller = new DacFxApplicationWebviewController( mockContext, vscodeWrapperStub, connectionManagerStub, @@ -158,7 +158,7 @@ suite("DataTierApplicationWebviewController", () => { }; const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); - const response = (await requestHandler!(params)) as DataTierApplicationResult; + const response = (await requestHandler!(params)) as DacFxApplicationResult; expect(dacFxServiceStub.deployDacpac).to.have.been.calledOnce; expect(dacFxServiceStub.deployDacpac).to.have.been.calledWith( @@ -560,7 +560,7 @@ suite("DataTierApplicationWebviewController", () => { ); const result = await requestHandler!({ databaseName: "AdventureWorks", - operationType: DataTierOperationType.Extract, + operationType: DacFxOperationType.Extract, }); expect(result.fullPath).to.include("myproject"); @@ -580,7 +580,7 @@ suite("DataTierApplicationWebviewController", () => { ); const result = await requestHandler!({ databaseName: "TestDB", - operationType: DataTierOperationType.Export, + operationType: DacFxOperationType.Export, }); expect(result.fullPath).to.not.include("workspace"); @@ -603,7 +603,7 @@ suite("DataTierApplicationWebviewController", () => { ); const result = await requestHandler!({ databaseName: "MyDB", - operationType: DataTierOperationType.Extract, + operationType: DacFxOperationType.Extract, }); expect(result.fullPath).to.include(".dacpac"); @@ -623,7 +623,7 @@ suite("DataTierApplicationWebviewController", () => { ); const result = await requestHandler!({ databaseName: "MyDB", - operationType: DataTierOperationType.Export, + operationType: DacFxOperationType.Export, }); expect(result.fullPath).to.include(".bacpac"); @@ -860,7 +860,7 @@ suite("DataTierApplicationWebviewController", () => { }); expect(response.isValid).to.be.false; - expect(response.errorMessage).to.equal(LocConstants.DataTierApplication.FileNotFound); + expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.FileNotFound); }); test("rejects empty file path", async () => { @@ -873,9 +873,7 @@ suite("DataTierApplicationWebviewController", () => { }); expect(response.isValid).to.be.false; - expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.FilePathRequired, - ); + expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.FilePathRequired); }); test("rejects invalid file extension", async () => { @@ -890,7 +888,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.InvalidFileExtension, + LocConstants.DacFxApplication.InvalidFileExtension, ); }); @@ -927,9 +925,7 @@ suite("DataTierApplicationWebviewController", () => { }); expect(response.isValid).to.be.true; - expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.FileAlreadyExists, - ); + expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.FileAlreadyExists); }); test("rejects output file path when directory doesn't exist", async () => { @@ -943,9 +939,7 @@ suite("DataTierApplicationWebviewController", () => { }); expect(response.isValid).to.be.false; - expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.DirectoryNotFound, - ); + expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.DirectoryNotFound); }); }); @@ -1033,7 +1027,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.isValid).to.be.true; expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.DatabaseAlreadyExists, + LocConstants.DacFxApplication.DatabaseAlreadyExists, ); }); @@ -1081,9 +1075,7 @@ suite("DataTierApplicationWebviewController", () => { }); expect(response.isValid).to.be.false; - expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.DatabaseNotFound, - ); + expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.DatabaseNotFound); }); test("rejects empty database name", async () => { @@ -1100,7 +1092,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.DatabaseNameRequired, + LocConstants.DacFxApplication.DatabaseNameRequired, ); }); @@ -1118,7 +1110,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.InvalidDatabaseName, + LocConstants.DacFxApplication.InvalidDatabaseName, ); }); @@ -1137,7 +1129,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.DatabaseNameTooLong, + LocConstants.DacFxApplication.DatabaseNameTooLong, ); }); @@ -1163,7 +1155,7 @@ suite("DataTierApplicationWebviewController", () => { expect(response.isValid).to.be.true; expect(response.errorMessage).to.equal( - LocConstants.DataTierApplication.DatabaseAlreadyExists, + LocConstants.DacFxApplication.DatabaseAlreadyExists, ); }); @@ -1195,7 +1187,7 @@ suite("DataTierApplicationWebviewController", () => { createController(); const cancelHandler = notificationHandlers.get( - CancelDataTierApplicationWebviewNotification.type.method, + CancelDacFxApplicationWebviewNotification.type.method, ); expect(cancelHandler, "Cancel handler was not registered").to.be.a("function"); @@ -1222,15 +1214,15 @@ suite("DataTierApplicationWebviewController", () => { // Mock user clicking "Deploy" button vscodeWrapperStub.showWarningMessageAdvanced.resolves( - LocConstants.DataTierApplication.DeployToExistingConfirm, + LocConstants.DacFxApplication.DeployToExistingConfirm, ); const response = await confirmHandler!(undefined); expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( - LocConstants.DataTierApplication.DeployToExistingMessage, + LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, - [LocConstants.DataTierApplication.DeployToExistingConfirm], + [LocConstants.DacFxApplication.DeployToExistingConfirm], ); expect(response.confirmed).to.be.true; }); @@ -1249,9 +1241,9 @@ suite("DataTierApplicationWebviewController", () => { const response = await confirmHandler!(undefined); expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( - LocConstants.DataTierApplication.DeployToExistingMessage, + LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, - [LocConstants.DataTierApplication.DeployToExistingConfirm], + [LocConstants.DacFxApplication.DeployToExistingConfirm], ); expect(response.confirmed).to.be.false; }); @@ -1270,9 +1262,9 @@ suite("DataTierApplicationWebviewController", () => { const response = await confirmHandler!(undefined); expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( - LocConstants.DataTierApplication.DeployToExistingMessage, + LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, - [LocConstants.DataTierApplication.DeployToExistingConfirm], + [LocConstants.DacFxApplication.DeployToExistingConfirm], ); expect(response.confirmed).to.be.false; }); @@ -1285,7 +1277,7 @@ suite("DataTierApplicationWebviewController", () => { expect(createWebviewPanelStub).to.have.been.calledOnce; expect(createWebviewPanelStub).to.have.been.calledWith( "mssql-react-webview", - LocConstants.DataTierApplication.Title, + LocConstants.DacFxApplication.Title, sinon.match.any, sinon.match.any, ); @@ -1310,9 +1302,8 @@ suite("DataTierApplicationWebviewController", () => { test("registers cancel notification handler", () => { createController(); - expect( - notificationHandlers.has(CancelDataTierApplicationWebviewNotification.type.method), - ).to.be.true; + expect(notificationHandlers.has(CancelDacFxApplicationWebviewNotification.type.method)) + .to.be.true; }); test("returns correct owner URI", () => { @@ -2182,7 +2173,7 @@ suite("DataTierApplicationWebviewController", () => { // Verify startActivity was called with correct parameters (twice: once for Load, once for the operation) expect(startActivityStub).to.have.been.calledTwice; expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxDeployDacpac, undefined, sinon.match({ isNewDatabase: "true" }), @@ -2308,7 +2299,7 @@ suite("DataTierApplicationWebviewController", () => { // Verify telemetry was started expect(startActivityStub).to.have.been.calledTwice; // Load + Extract expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxExtractDacpac, ); @@ -2395,7 +2386,7 @@ suite("DataTierApplicationWebviewController", () => { expect(startActivityStub).to.have.been.calledTwice; // Load + Import expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxImportBacpac, ); @@ -2474,7 +2465,7 @@ suite("DataTierApplicationWebviewController", () => { expect(startActivityStub).to.have.been.calledTwice; // Load + Export expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DataTierApplication, + TelemetryViews.DacFxApplication, TelemetryActions.DacFxExportBacpac, ); diff --git a/test/unit/mainController.test.ts b/test/unit/mainController.test.ts index d4f59c81b9..13353737bd 100644 --- a/test/unit/mainController.test.ts +++ b/test/unit/mainController.test.ts @@ -340,11 +340,11 @@ suite("MainController Tests", function () { }); suite("Data-Tier Application Commands", () => { - test("cmdDataTierApplication command is registered", async () => { + test("cmdDacFxApplication command is registered", async () => { const commands = await vscode.commands.getCommands(true); assert.ok( - commands.includes(Constants.cmdDataTierApplication), - "Expected cmdDataTierApplication to be registered", + commands.includes(Constants.cmdDacFxApplication), + "Expected cmdDacFxApplication to be registered", ); }); From a8671e67b5dc006b0f4234621bdfb73415086ff6 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 10:33:18 -0600 Subject: [PATCH 35/79] refactor from explicit importing to * per pr request --- .../dacFxApplicationWebviewController.ts | 159 +++++++++--------- 1 file changed, 81 insertions(+), 78 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 0498e9213a..1828bedd50 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -16,32 +16,7 @@ import VscodeWrapper from "./vscodeWrapper"; import * as LocConstants from "../constants/locConstants"; import { startActivity } from "../telemetry/telemetry"; import { TelemetryViews, TelemetryActions, ActivityStatus } from "../sharedInterfaces/telemetry"; -import { - BrowseInputFileWebviewRequest, - BrowseOutputFileWebviewRequest, - CancelDacFxApplicationWebviewNotification, - ConfirmDeployToExistingWebviewRequest, - ConnectionProfile, - ConnectToServerWebviewRequest, - DacFxApplicationResult, - DacFxApplicationWebviewState, - DacFxOperationType, - DeployDacpacParams, - DeployDacpacWebviewRequest, - ExportBacpacParams, - ExportBacpacWebviewRequest, - ExtractDacpacParams, - ExtractDacpacWebviewRequest, - GetSuggestedDatabaseNameWebviewRequest, - GetSuggestedOutputPathWebviewRequest, - ImportBacpacParams, - ImportBacpacWebviewRequest, - InitializeConnectionWebviewRequest, - ListConnectionsWebviewRequest, - ListDatabasesWebviewRequest, - ValidateDatabaseNameWebviewRequest, - ValidateFilePathWebviewRequest, -} from "../sharedInterfaces/dacFxApplication"; +import * as dacFxApplication from "../sharedInterfaces/dacFxApplication"; import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; @@ -50,9 +25,9 @@ import { ListDatabasesRequest } from "../models/contracts/connection"; * Manages DACPAC and BACPAC operations (Deploy, Extract, Import, Export) using the Data-tier Application Framework (DacFx). */ export class DacFxApplicationWebviewController extends ReactWebviewPanelController< - DacFxApplicationWebviewState, + dacFxApplication.DacFxApplicationWebviewState, void, - DacFxApplicationResult + dacFxApplication.DacFxApplicationResult > { private _ownerUri: string; @@ -61,7 +36,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll vscodeWrapper: VscodeWrapper, private connectionManager: ConnectionManager, private dacFxService: DacFxService, - initialState: DacFxApplicationWebviewState, + initialState: dacFxApplication.DacFxApplicationWebviewState, ownerUri: string, ) { super(context, vscodeWrapper, "dacFxApplication", "dacFxApplication", initialState, { @@ -83,50 +58,65 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll */ private registerRpcHandlers(): void { // Deploy DACPAC request handler - this.onRequest(DeployDacpacWebviewRequest.type, async (params: DeployDacpacParams) => { - return await this.handleDeployDacpac(params); - }); + this.onRequest( + dacFxApplication.DeployDacpacWebviewRequest.type, + async (params: dacFxApplication.DeployDacpacParams) => { + return await this.handleDeployDacpac(params); + }, + ); // Extract DACPAC request handler - this.onRequest(ExtractDacpacWebviewRequest.type, async (params: ExtractDacpacParams) => { - return await this.handleExtractDacpac(params); - }); + this.onRequest( + dacFxApplication.ExtractDacpacWebviewRequest.type, + async (params: dacFxApplication.ExtractDacpacParams) => { + return await this.handleExtractDacpac(params); + }, + ); // Import BACPAC request handler - this.onRequest(ImportBacpacWebviewRequest.type, async (params: ImportBacpacParams) => { - return await this.handleImportBacpac(params); - }); + this.onRequest( + dacFxApplication.ImportBacpacWebviewRequest.type, + async (params: dacFxApplication.ImportBacpacParams) => { + return await this.handleImportBacpac(params); + }, + ); // Export BACPAC request handler - this.onRequest(ExportBacpacWebviewRequest.type, async (params: ExportBacpacParams) => { - return await this.handleExportBacpac(params); - }); + this.onRequest( + dacFxApplication.ExportBacpacWebviewRequest.type, + async (params: dacFxApplication.ExportBacpacParams) => { + return await this.handleExportBacpac(params); + }, + ); // Validate file path request handler this.onRequest( - ValidateFilePathWebviewRequest.type, + dacFxApplication.ValidateFilePathWebviewRequest.type, async (params: { filePath: string; shouldExist: boolean }) => { return this.validateFilePath(params.filePath, params.shouldExist); }, ); // List databases request handler - this.onRequest(ListDatabasesWebviewRequest.type, async (params: { ownerUri: string }) => { - if (!params.ownerUri || params.ownerUri.trim() === "") { - this.logger.error("Cannot list databases: ownerUri is empty"); - return { databases: [] }; - } - return await this.listDatabases(params.ownerUri); - }); + this.onRequest( + dacFxApplication.ListDatabasesWebviewRequest.type, + async (params: { ownerUri: string }) => { + if (!params.ownerUri || params.ownerUri.trim() === "") { + this.logger.error("Cannot list databases: ownerUri is empty"); + return { databases: [] }; + } + return await this.listDatabases(params.ownerUri); + }, + ); // Validate database name request handler this.onRequest( - ValidateDatabaseNameWebviewRequest.type, + dacFxApplication.ValidateDatabaseNameWebviewRequest.type, async (params: { databaseName: string; ownerUri: string; shouldNotExist: boolean; - operationType?: DacFxOperationType; + operationType?: dacFxApplication.DacFxOperationType; }) => { if (!params.ownerUri || params.ownerUri.trim() === "") { this.logger.error("Cannot validate database name: ownerUri is empty"); @@ -146,13 +136,13 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll ); // List connections request handler - this.onRequest(ListConnectionsWebviewRequest.type, async () => { + this.onRequest(dacFxApplication.ListConnectionsWebviewRequest.type, async () => { return await this.listConnections(); }); // Initialize connection request handler this.onRequest( - InitializeConnectionWebviewRequest.type, + dacFxApplication.InitializeConnectionWebviewRequest.type, async (params: { initialServerName?: string; initialDatabaseName?: string; @@ -165,7 +155,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Connect to server request handler this.onRequest( - ConnectToServerWebviewRequest.type, + dacFxApplication.ConnectToServerWebviewRequest.type, async (params: { profileId: string }) => { return await this.connectToServer(params.profileId); }, @@ -173,7 +163,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Browse for input file (DACPAC or BACPAC) request handler this.onRequest( - BrowseInputFileWebviewRequest.type, + dacFxApplication.BrowseInputFileWebviewRequest.type, async (params: { fileExtension: string }) => { const fileUri = await vscode.window.showOpenDialog({ canSelectFiles: true, @@ -195,7 +185,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Browse for output file (DACPAC or BACPAC) request handler this.onRequest( - BrowseOutputFileWebviewRequest.type, + dacFxApplication.BrowseOutputFileWebviewRequest.type, async (params: { fileExtension: string; defaultFileName?: string }) => { const defaultFileName = params.defaultFileName || `database.${params.fileExtension}`; @@ -222,10 +212,15 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get default output path without showing dialog this.onRequest( - GetSuggestedOutputPathWebviewRequest.type, - async (params: { databaseName: string; operationType: DacFxOperationType }) => { + dacFxApplication.GetSuggestedOutputPathWebviewRequest.type, + async (params: { + databaseName: string; + operationType: dacFxApplication.DacFxOperationType; + }) => { const fileExtension = - params.operationType === DacFxOperationType.Extract ? "dacpac" : "bacpac"; + params.operationType === dacFxApplication.DacFxOperationType.Extract + ? "dacpac" + : "bacpac"; // Format timestamp as yyyy-MM-dd-HH-mm const now = new Date(); @@ -250,7 +245,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get suggested database name from file path this.onRequest( - GetSuggestedDatabaseNameWebviewRequest.type, + dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, async (params: { filePath: string }) => { // Extract filename without directory path const fileName = path.basename(params.filePath); @@ -264,7 +259,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll ); // Confirm deploy to existing database request handler - this.onRequest(ConfirmDeployToExistingWebviewRequest.type, async () => { + this.onRequest(dacFxApplication.ConfirmDeployToExistingWebviewRequest.type, async () => { const result = await this.vscodeWrapper.showWarningMessageAdvanced( LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, @@ -277,7 +272,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll }); // Cancel operation notification handler - this.onNotification(CancelDacFxApplicationWebviewNotification.type, () => { + this.onNotification(dacFxApplication.CancelDacFxApplicationWebviewNotification.type, () => { this.dialogResult.resolve(undefined); this.panel.dispose(); }); @@ -286,7 +281,9 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll /** * Handles deploying a DACPAC file to a database */ - private async handleDeployDacpac(params: DeployDacpacParams): Promise { + private async handleDeployDacpac( + params: dacFxApplication.DeployDacpacParams, + ): Promise { const activity = startActivity( TelemetryViews.DacFxApplication, TelemetryActions.DacFxDeployDacpac, @@ -305,7 +302,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: DacFxApplicationResult = { + const appResult: dacFxApplication.DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -337,8 +334,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Handles extracting a DACPAC file from a database */ private async handleExtractDacpac( - params: ExtractDacpacParams, - ): Promise { + params: dacFxApplication.ExtractDacpacParams, + ): Promise { const activity = startActivity( TelemetryViews.DacFxApplication, TelemetryActions.DacFxExtractDacpac, @@ -354,7 +351,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: DacFxApplicationResult = { + const appResult: dacFxApplication.DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -385,7 +382,9 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll /** * Handles importing a BACPAC file to create a new database */ - private async handleImportBacpac(params: ImportBacpacParams): Promise { + private async handleImportBacpac( + params: dacFxApplication.ImportBacpacParams, + ): Promise { const activity = startActivity( TelemetryViews.DacFxApplication, TelemetryActions.DacFxImportBacpac, @@ -399,7 +398,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: DacFxApplicationResult = { + const appResult: dacFxApplication.DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -430,7 +429,9 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll /** * Handles exporting a database to a BACPAC file */ - private async handleExportBacpac(params: ExportBacpacParams): Promise { + private async handleExportBacpac( + params: dacFxApplication.ExportBacpacParams, + ): Promise { const activity = startActivity( TelemetryViews.DacFxApplication, TelemetryActions.DacFxExportBacpac, @@ -444,7 +445,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: DacFxApplicationResult = { + const appResult: dacFxApplication.DacFxApplicationResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -546,9 +547,11 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll /** * Lists all available connections (recent and active) */ - private async listConnections(): Promise<{ connections: ConnectionProfile[] }> { + private async listConnections(): Promise<{ + connections: dacFxApplication.ConnectionProfile[]; + }> { try { - const connections: ConnectionProfile[] = []; + const connections: dacFxApplication.ConnectionProfile[] = []; // Get recently used connections from connection store const recentConnections = @@ -633,8 +636,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll initialOwnerUri?: string; initialProfileId?: string; }): Promise<{ - connections: ConnectionProfile[]; - selectedConnection?: ConnectionProfile; + connections: dacFxApplication.ConnectionProfile[]; + selectedConnection?: dacFxApplication.ConnectionProfile; ownerUri?: string; autoConnected: boolean; errorMessage?: string; @@ -644,7 +647,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll const { connections } = await this.listConnections(); // Helper to find matching connection - const findMatchingConnection = (): ConnectionProfile | undefined => { + const findMatchingConnection = (): dacFxApplication.ConnectionProfile | undefined => { // Priority 1: Match by profile ID if provided if (params.initialProfileId) { const byProfileId = connections.find( @@ -900,7 +903,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll databaseName: string, ownerUri: string, shouldNotExist: boolean, - operationType?: DacFxOperationType, + operationType?: dacFxApplication.DacFxOperationType, ): Promise<{ isValid: boolean; errorMessage?: string }> { if (!databaseName || databaseName.trim() === "") { return { @@ -940,7 +943,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // This ensures confirmation dialog is shown in both cases: // 1. User selected "New Database" but database already exists (shouldNotExist=true) // 2. User selected "Existing Database" and selected existing database (shouldNotExist=false) - if (operationType === DacFxOperationType.Deploy && exists) { + if (operationType === dacFxApplication.DacFxOperationType.Deploy && exists) { return { isValid: true, // Allow the operation but with a warning errorMessage: LocConstants.DacFxApplication.DatabaseAlreadyExists, From bf2bbd16c3a2f92b3e982a89a8d1522455ab0279 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 10:58:14 -0600 Subject: [PATCH 36/79] breaking the form into pieces per PR comment --- .../ApplicationInfoSection.tsx | 62 +++ .../DacFxApplication/FilePathSection.tsx | 100 +++++ .../DacFxApplication/OperationTypeSection.tsx | 88 ++++ .../ServerSelectionSection.tsx | 92 ++++ .../SourceDatabaseSection.tsx | 99 +++++ .../TargetDatabaseSection.tsx | 131 ++++++ .../DacFxApplication/dacFxApplicationForm.tsx | 405 +++--------------- 7 files changed, 638 insertions(+), 339 deletions(-) create mode 100644 src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx create mode 100644 src/reactviews/pages/DacFxApplication/FilePathSection.tsx create mode 100644 src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx create mode 100644 src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx create mode 100644 src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx create mode 100644 src/reactviews/pages/DacFxApplication/TargetDatabaseSection.tsx diff --git a/src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx b/src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx new file mode 100644 index 0000000000..2c604da053 --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx @@ -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 { Field, Input, makeStyles } from "@fluentui/react-components"; +import { locConstants } from "../../common/locConstants"; + +/** + * Default application version for DACPAC extraction + */ +const DEFAULT_APPLICATION_VERSION = "1.0.0"; + +interface ApplicationInfoSectionProps { + applicationName: string; + setApplicationName: (value: string) => void; + applicationVersion: string; + setApplicationVersion: (value: string) => void; + isOperationInProgress: boolean; +} + +const useStyles = makeStyles({ + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, +}); + +export const ApplicationInfoSection = ({ + applicationName, + setApplicationName, + applicationVersion, + setApplicationVersion, + isOperationInProgress, +}: ApplicationInfoSectionProps) => { + const classes = useStyles(); + + return ( +
+ + setApplicationName(data.value)} + placeholder={locConstants.dacFxApplication.enterApplicationName} + disabled={isOperationInProgress} + aria-label={locConstants.dacFxApplication.applicationNameLabel} + /> + + + + setApplicationVersion(data.value)} + placeholder={DEFAULT_APPLICATION_VERSION} + disabled={isOperationInProgress} + aria-label={locConstants.dacFxApplication.applicationVersionLabel} + /> + +
+ ); +}; diff --git a/src/reactviews/pages/DacFxApplication/FilePathSection.tsx b/src/reactviews/pages/DacFxApplication/FilePathSection.tsx new file mode 100644 index 0000000000..770cb2bc5b --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/FilePathSection.tsx @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Button, Field, Input, makeStyles } from "@fluentui/react-components"; +import { FolderOpen20Regular } from "@fluentui/react-icons"; +import { locConstants } from "../../common/locConstants"; + +/** + * Validation message with severity level + */ +interface ValidationMessage { + message: string; + severity: "error" | "warning"; +} + +interface FilePathSectionProps { + filePath: string; + setFilePath: (value: string) => void; + requiresInputFile: boolean; + isOperationInProgress: boolean; + validationMessages: Record; + onBrowseFile: () => void; + onFilePathChange: (value: string) => void; +} + +const useStyles = makeStyles({ + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, + fileInputGroup: { + display: "flex", + gap: "8px", + alignItems: "flex-end", + }, + fileInput: { + flexGrow: 1, + }, +}); + +export const FilePathSection = ({ + filePath, + requiresInputFile, + isOperationInProgress, + validationMessages, + onBrowseFile, + onFilePathChange, +}: FilePathSectionProps) => { + const classes = useStyles(); + + return ( +
+ +
+ onFilePathChange(data.value)} + placeholder={ + requiresInputFile + ? locConstants.dacFxApplication.selectPackageFile + : locConstants.dacFxApplication.selectOutputFile + } + disabled={isOperationInProgress} + aria-label={ + requiresInputFile + ? locConstants.dacFxApplication.packageFileLabel + : locConstants.dacFxApplication.outputFileLabel + } + /> + +
+
+
+ ); +}; diff --git a/src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx b/src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx new file mode 100644 index 0000000000..ef76c21fda --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Field, makeStyles, Radio, RadioGroup } from "@fluentui/react-components"; +import { DacFxOperationType } from "../../../sharedInterfaces/dacFxApplication"; +import { locConstants } from "../../common/locConstants"; + +interface OperationTypeSectionProps { + operationType: DacFxOperationType; + setOperationType: (value: DacFxOperationType) => void; + isOperationInProgress: boolean; + onOperationTypeChange?: () => void; +} + +const useStyles = makeStyles({ + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, +}); + +export const OperationTypeSection = ({ + operationType, + setOperationType, + isOperationInProgress, + onOperationTypeChange, +}: OperationTypeSectionProps) => { + const classes = useStyles(); + + return ( +
+ + { + setOperationType(data.value as DacFxOperationType); + onOperationTypeChange?.(); + }} + disabled={isOperationInProgress} + aria-label={locConstants.dacFxApplication.operationLabel}> + + + + + + +
+ ); +}; diff --git a/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx b/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx new file mode 100644 index 0000000000..656318eb33 --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dropdown, Field, makeStyles, Option, Spinner } from "@fluentui/react-components"; +import { ConnectionProfile } from "../../../sharedInterfaces/dacFxApplication"; +import { locConstants } from "../../common/locConstants"; + +/** + * Validation message with severity level + */ +interface ValidationMessage { + message: string; + severity: "error" | "warning"; +} + +interface ServerSelectionSectionProps { + selectedProfileId: string; + availableConnections: ConnectionProfile[]; + isConnecting: boolean; + isOperationInProgress: boolean; + validationMessages: Record; + onServerChange: (profileId: string) => void; +} + +const useStyles = makeStyles({ + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, +}); + +export const ServerSelectionSection = ({ + selectedProfileId, + availableConnections, + isConnecting, + isOperationInProgress, + validationMessages, + onServerChange, +}: ServerSelectionSectionProps) => { + const classes = useStyles(); + + return ( +
+ + {isConnecting ? ( + + ) : ( + conn.profileId === selectedProfileId, + )?.displayName || "" + : "" + } + selectedOptions={selectedProfileId ? [selectedProfileId] : []} + onOptionSelect={(_, data) => { + onServerChange(data.optionValue as string); + }} + disabled={isOperationInProgress || availableConnections.length === 0} + aria-label={locConstants.dacFxApplication.serverLabel}> + {availableConnections.length === 0 ? ( + + ) : ( + availableConnections.map((conn) => ( + + )) + )} + + )} + +
+ ); +}; diff --git a/src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx b/src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx new file mode 100644 index 0000000000..2998f396ff --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Dropdown, Field, Input, makeStyles, Option } from "@fluentui/react-components"; +import { locConstants } from "../../common/locConstants"; + +/** + * Validation message with severity level + */ +interface ValidationMessage { + message: string; + severity: "error" | "warning"; +} + +interface SourceDatabaseSectionProps { + databaseName: string; + setDatabaseName: (value: string) => void; + availableDatabases: string[]; + isOperationInProgress: boolean; + ownerUri: string; + validationMessages: Record; + showDatabaseSource: boolean; + showNewDatabase: boolean; +} + +const useStyles = makeStyles({ + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, +}); + +export const SourceDatabaseSection = ({ + databaseName, + setDatabaseName, + availableDatabases, + isOperationInProgress, + ownerUri, + validationMessages, + showDatabaseSource, + showNewDatabase, +}: SourceDatabaseSectionProps) => { + const classes = useStyles(); + + return ( +
+ {showDatabaseSource ? ( + + setDatabaseName(data.optionText || "")} + disabled={isOperationInProgress || !ownerUri} + aria-label={locConstants.dacFxApplication.sourceDatabaseLabel}> + {availableDatabases.map((db) => ( + + ))} + + + ) : ( + showNewDatabase && ( + + setDatabaseName(data.value)} + placeholder={locConstants.dacFxApplication.enterDatabaseName} + disabled={isOperationInProgress} + aria-label={locConstants.dacFxApplication.databaseNameLabel} + /> + + ) + )} +
+ ); +}; diff --git a/src/reactviews/pages/DacFxApplication/TargetDatabaseSection.tsx b/src/reactviews/pages/DacFxApplication/TargetDatabaseSection.tsx new file mode 100644 index 0000000000..704184b857 --- /dev/null +++ b/src/reactviews/pages/DacFxApplication/TargetDatabaseSection.tsx @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Dropdown, + Field, + Input, + Label, + makeStyles, + Option, + Radio, + RadioGroup, +} from "@fluentui/react-components"; +import { locConstants } from "../../common/locConstants"; + +/** + * Validation message with severity level + */ +interface ValidationMessage { + message: string; + severity: "error" | "warning"; +} + +interface TargetDatabaseSectionProps { + databaseName: string; + setDatabaseName: (value: string) => void; + isNewDatabase: boolean; + setIsNewDatabase: (value: boolean) => void; + availableDatabases: string[]; + isOperationInProgress: boolean; + ownerUri: string; + validationMessages: Record; +} + +const useStyles = makeStyles({ + section: { + display: "flex", + flexDirection: "column", + gap: "12px", + }, + radioGroup: { + display: "flex", + flexDirection: "column", + gap: "8px", + }, +}); + +export const TargetDatabaseSection = ({ + databaseName, + setDatabaseName, + isNewDatabase, + setIsNewDatabase, + availableDatabases, + isOperationInProgress, + ownerUri, + validationMessages, +}: TargetDatabaseSectionProps) => { + const classes = useStyles(); + + return ( +
+ + setIsNewDatabase(data.value === "new")} + className={classes.radioGroup} + aria-label={locConstants.dacFxApplication.targetDatabaseLabel}> + + + + + {isNewDatabase ? ( + + setDatabaseName(data.value)} + placeholder={locConstants.dacFxApplication.enterDatabaseName} + disabled={isOperationInProgress} + aria-label={locConstants.dacFxApplication.databaseNameLabel} + /> + + ) : ( + + setDatabaseName(data.optionText || "")} + disabled={isOperationInProgress || !ownerUri} + aria-label={locConstants.dacFxApplication.databaseNameLabel}> + {availableDatabases.map((db) => ( + + ))} + + + )} +
+ ); +}; diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index d082512fe6..072318125b 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -3,20 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - Button, - Dropdown, - Field, - Input, - Label, - makeStyles, - Option, - Radio, - RadioGroup, - Spinner, - tokens, -} from "@fluentui/react-components"; -import { FolderOpen20Regular, DatabaseArrowRight20Regular } from "@fluentui/react-icons"; +import { Button, makeStyles, tokens } from "@fluentui/react-components"; +import { DatabaseArrowRight20Regular } from "@fluentui/react-icons"; import { useState, useEffect, useContext } from "react"; import { BrowseInputFileWebviewRequest, @@ -40,6 +28,12 @@ import { import { DacFxApplicationContext } from "./dacFxApplicationStateProvider"; import { useDacFxApplicationSelector } from "./dacFxApplicationSelector"; import { locConstants } from "../../common/locConstants"; +import { TargetDatabaseSection } from "./TargetDatabaseSection"; +import { SourceDatabaseSection } from "./SourceDatabaseSection"; +import { ApplicationInfoSection } from "./ApplicationInfoSection"; +import { OperationTypeSection } from "./OperationTypeSection"; +import { ServerSelectionSection } from "./ServerSelectionSection"; +import { FilePathSection } from "./FilePathSection"; /** * Validation message with severity level @@ -80,24 +74,6 @@ const useStyles = makeStyles({ color: tokens.colorNeutralForeground2, marginBottom: "16px", }, - section: { - display: "flex", - flexDirection: "column", - gap: "12px", - }, - fileInputGroup: { - display: "flex", - gap: "8px", - alignItems: "flex-end", - }, - fileInput: { - flexGrow: 1, - }, - radioGroup: { - display: "flex", - flexDirection: "column", - gap: "8px", - }, actions: { display: "flex", gap: "8px", @@ -106,17 +82,6 @@ const useStyles = makeStyles({ paddingTop: "16px", borderTop: `1px solid ${tokens.colorNeutralStroke2}`, }, - progressContainer: { - display: "flex", - flexDirection: "column", - gap: "8px", - padding: "12px", - backgroundColor: tokens.colorNeutralBackground3, - borderRadius: tokens.borderRadiusMedium, - }, - warningMessage: { - marginTop: "8px", - }, }); export const DacFxApplicationForm = () => { @@ -753,310 +718,72 @@ export const DacFxApplicationForm = () => {
-
- - { - setOperationType(data.value as DacFxOperationType); - setValidationMessages({}); - // Reset file path when switching operation types - // Import/Deploy need empty (browse for existing file) - // Export/Extract will be set when database name changes - setFilePath(""); - }} - disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.operationLabel}> - - - - - - -
- -
- - {isConnecting ? ( - - ) : ( - conn.profileId === selectedProfileId, - )?.displayName || "" - : "" - } - selectedOptions={selectedProfileId ? [selectedProfileId] : []} - onOptionSelect={(_, data) => { - void handleServerChange(data.optionValue as string); - }} - disabled={ - isOperationInProgress || availableConnections.length === 0 - } - aria-label={locConstants.dacFxApplication.serverLabel}> - {availableConnections.length === 0 ? ( - - ) : ( - availableConnections.map((conn) => ( - - )) - )} - - )} - -
- -
- -
- handleFilePathChange(data.value)} - placeholder={ - requiresInputFile - ? locConstants.dacFxApplication.selectPackageFile - : locConstants.dacFxApplication.selectOutputFile - } - disabled={isOperationInProgress} - aria-label={ - requiresInputFile - ? locConstants.dacFxApplication.packageFileLabel - : locConstants.dacFxApplication.outputFileLabel - } - /> - -
-
-
+ { + setValidationMessages({}); + // Reset file path when switching operation types + // Import/Deploy need empty (browse for existing file) + // Export/Extract will be set when database name changes + setFilePath(""); + }} + /> + + void handleServerChange(profileId)} + /> + + {showDatabaseTarget && ( -
- - setIsNewDatabase(data.value === "new")} - className={classes.radioGroup} - aria-label={locConstants.dacFxApplication.targetDatabaseLabel}> - - - - - {isNewDatabase ? ( - - setDatabaseName(data.value)} - placeholder={locConstants.dacFxApplication.enterDatabaseName} - disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.databaseNameLabel} - /> - - ) : ( - - - setDatabaseName(data.optionText || "") - } - disabled={isOperationInProgress || !ownerUri} - aria-label={locConstants.dacFxApplication.databaseNameLabel}> - {availableDatabases.map((db) => ( - - ))} - - - )} -
+ )} {(showDatabaseSource || showNewDatabase) && ( -
- {showDatabaseSource ? ( - - - setDatabaseName(data.optionText || "") - } - disabled={isOperationInProgress || !ownerUri} - aria-label={locConstants.dacFxApplication.sourceDatabaseLabel}> - {availableDatabases.map((db) => ( - - ))} - - - ) : ( - - setDatabaseName(data.value)} - placeholder={locConstants.dacFxApplication.enterDatabaseName} - disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.databaseNameLabel} - /> - - )} -
+ )} {showApplicationInfo && ( -
- - setApplicationName(data.value)} - placeholder={locConstants.dacFxApplication.enterApplicationName} - disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.applicationNameLabel} - /> - - - - setApplicationVersion(data.value)} - placeholder={DEFAULT_APPLICATION_VERSION} - disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.applicationVersionLabel} - /> - -
+ )}
From d743607bf5a5233e5825f53c74627b09db767c7c Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:09:16 -0600 Subject: [PATCH 37/79] refactor to simplify the registration calls using a shared method for the common logic --- src/controllers/mainController.ts | 176 ++++-------------------------- 1 file changed, 20 insertions(+), 156 deletions(-) diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 2418bcec1e..2cab3afb18 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -1787,122 +1787,16 @@ export default class MainController implements vscode.Disposable { ), ); - // Data-tier Application - Main command - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdDacFxApplication, - async (node?: TreeNodeInfo) => { - const connectionProfile = node?.connectionProfile; - const ownerUri = connectionProfile - ? this._connectionMgr.getUriForConnection(connectionProfile) - : ""; - const serverName = connectionProfile?.server || ""; - const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; - const profileId = connectionProfile - ? connectionProfile.id || - `${connectionProfile.server}_${connectionProfile.database || ""}` - : undefined; - - const initialState: DacFxApplicationWebviewState = { - ownerUri, - serverName, - databaseName, - selectedProfileId: profileId, - operationType: DacFxOperationType.Deploy, - }; - - const controller = new DacFxApplicationWebviewController( - this._context, - this._vscodeWrapper, - this._connectionMgr, - this.dacFxService, - initialState, - ownerUri, - ); - await controller.revealToForeground(); - }, - ), - ); - - // Data-tier Application - Deploy DACPAC - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdDeployDacpac, - async (node?: TreeNodeInfo) => { - const connectionProfile = node?.connectionProfile; - const ownerUri = connectionProfile - ? this._connectionMgr.getUriForConnection(connectionProfile) - : ""; - const serverName = connectionProfile?.server || ""; - const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; - const profileId = connectionProfile - ? connectionProfile.id || - `${connectionProfile.server}_${connectionProfile.database || ""}` - : undefined; - - const initialState: DacFxApplicationWebviewState = { - ownerUri, - serverName, - databaseName, - selectedProfileId: profileId, - operationType: DacFxOperationType.Deploy, - }; - - const controller = new DacFxApplicationWebviewController( - this._context, - this._vscodeWrapper, - this._connectionMgr, - this.dacFxService, - initialState, - ownerUri, - ); - await controller.revealToForeground(); - }, - ), - ); - - // Data-tier Application - Extract DACPAC - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdExtractDacpac, - async (node?: TreeNodeInfo) => { - const connectionProfile = node?.connectionProfile; - const ownerUri = connectionProfile - ? this._connectionMgr.getUriForConnection(connectionProfile) - : ""; - const serverName = connectionProfile?.server || ""; - const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; - const profileId = connectionProfile - ? connectionProfile.id || - `${connectionProfile.server}_${connectionProfile.database || ""}` - : undefined; - - const initialState: DacFxApplicationWebviewState = { - ownerUri, - serverName, - databaseName, - selectedProfileId: profileId, - operationType: DacFxOperationType.Extract, - }; - - const controller = new DacFxApplicationWebviewController( - this._context, - this._vscodeWrapper, - this._connectionMgr, - this.dacFxService, - initialState, - ownerUri, - ); - await controller.revealToForeground(); - }, - ), - ); - - // Data-tier Application - Import BACPAC - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdImportBacpac, - async (node?: TreeNodeInfo) => { + /** + * Helper function to register Data-tier Application commands + * Reduces code duplication across Deploy, Extract, Import, and Export operations + */ + const registerDacFxCommand = ( + commandId: string, + operationType: DacFxOperationType, + ): void => { + this._context.subscriptions.push( + vscode.commands.registerCommand(commandId, async (node?: TreeNodeInfo) => { const connectionProfile = node?.connectionProfile; const ownerUri = connectionProfile ? this._connectionMgr.getUriForConnection(connectionProfile) @@ -1919,7 +1813,7 @@ export default class MainController implements vscode.Disposable { serverName, databaseName, selectedProfileId: profileId, - operationType: DacFxOperationType.Import, + operationType, }; const controller = new DacFxApplicationWebviewController( @@ -1931,46 +1825,16 @@ export default class MainController implements vscode.Disposable { ownerUri, ); await controller.revealToForeground(); - }, - ), - ); - - // Data-tier Application - Export BACPAC - this._context.subscriptions.push( - vscode.commands.registerCommand( - Constants.cmdExportBacpac, - async (node?: TreeNodeInfo) => { - const connectionProfile = node?.connectionProfile; - const ownerUri = connectionProfile - ? this._connectionMgr.getUriForConnection(connectionProfile) - : ""; - const serverName = connectionProfile?.server || ""; - const databaseName = node ? ObjectExplorerUtils.getDatabaseName(node) : ""; - const profileId = connectionProfile - ? connectionProfile.id || - `${connectionProfile.server}_${connectionProfile.database || ""}` - : undefined; - - const initialState: DacFxApplicationWebviewState = { - ownerUri, - serverName, - databaseName, - selectedProfileId: profileId, - operationType: DacFxOperationType.Export, - }; + }), + ); + }; - const controller = new DacFxApplicationWebviewController( - this._context, - this._vscodeWrapper, - this._connectionMgr, - this.dacFxService, - initialState, - ownerUri, - ); - await controller.revealToForeground(); - }, - ), - ); + // Data-tier Application commands + registerDacFxCommand(Constants.cmdDacFxApplication, DacFxOperationType.Deploy); + registerDacFxCommand(Constants.cmdDeployDacpac, DacFxOperationType.Deploy); + registerDacFxCommand(Constants.cmdExtractDacpac, DacFxOperationType.Extract); + registerDacFxCommand(Constants.cmdImportBacpac, DacFxOperationType.Import); + registerDacFxCommand(Constants.cmdExportBacpac, DacFxOperationType.Export); // Copy object name command this._context.subscriptions.push( From 4c7ee5792cbd4695e3304fd076af83804f7bfe24 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:21:48 -0600 Subject: [PATCH 38/79] pr comment to combine imports --- .../DacFxApplication/dacFxApplicationForm.tsx | 128 ++++++++---------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index 072318125b..493b611dbd 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -6,34 +6,16 @@ import { Button, makeStyles, tokens } from "@fluentui/react-components"; import { DatabaseArrowRight20Regular } from "@fluentui/react-icons"; import { useState, useEffect, useContext } from "react"; -import { - BrowseInputFileWebviewRequest, - BrowseOutputFileWebviewRequest, - ConnectionProfile, - ConnectToServerWebviewRequest, - DacFxOperationType, - DeployDacpacWebviewRequest, - ExtractDacpacWebviewRequest, - ImportBacpacWebviewRequest, - ExportBacpacWebviewRequest, - GetSuggestedDatabaseNameWebviewRequest, - GetSuggestedOutputPathWebviewRequest, - InitializeConnectionWebviewRequest, - ValidateFilePathWebviewRequest, - ListDatabasesWebviewRequest, - ValidateDatabaseNameWebviewRequest, - CancelDacFxApplicationWebviewNotification, - ConfirmDeployToExistingWebviewRequest, -} from "../../../sharedInterfaces/dacFxApplication"; -import { DacFxApplicationContext } from "./dacFxApplicationStateProvider"; -import { useDacFxApplicationSelector } from "./dacFxApplicationSelector"; +import * as dacFxApplication from "../../../sharedInterfaces/dacFxApplication"; import { locConstants } from "../../common/locConstants"; -import { TargetDatabaseSection } from "./TargetDatabaseSection"; -import { SourceDatabaseSection } from "./SourceDatabaseSection"; import { ApplicationInfoSection } from "./ApplicationInfoSection"; +import { DacFxApplicationContext } from "./dacFxApplicationStateProvider"; +import { useDacFxApplicationSelector } from "./dacFxApplicationSelector"; +import { FilePathSection } from "./FilePathSection"; import { OperationTypeSection } from "./OperationTypeSection"; import { ServerSelectionSection } from "./ServerSelectionSection"; -import { FilePathSection } from "./FilePathSection"; +import { SourceDatabaseSection } from "./SourceDatabaseSection"; +import { TargetDatabaseSection } from "./TargetDatabaseSection"; /** * Validation message with severity level @@ -98,8 +80,8 @@ export const DacFxApplicationForm = () => { ); // Local state - const [operationType, setOperationType] = useState( - initialOperationType || DacFxOperationType.Deploy, + const [operationType, setOperationType] = useState( + initialOperationType || dacFxApplication.DacFxOperationType.Deploy, ); const [filePath, setFilePath] = useState(""); const [databaseName, setDatabaseName] = useState(initialDatabaseName || ""); @@ -113,7 +95,9 @@ export const DacFxApplicationForm = () => { const [validationMessages, setValidationMessages] = useState>( {}, ); - const [availableConnections, setAvailableConnections] = useState([]); + const [availableConnections, setAvailableConnections] = useState< + dacFxApplication.ConnectionProfile[] + >([]); const [selectedProfileId, setSelectedProfileId] = useState( initialSelectedProfileId || "", ); @@ -128,7 +112,7 @@ export const DacFxApplicationForm = () => { return () => { if (isConnecting || isOperationInProgress) { void context?.extensionRpc?.sendNotification( - CancelDacFxApplicationWebviewNotification.type, + dacFxApplication.CancelDacFxApplicationWebviewNotification.type, ); } }; @@ -138,9 +122,9 @@ export const DacFxApplicationForm = () => { useEffect(() => { if ( ownerUri && - (operationType === DacFxOperationType.Deploy || - operationType === DacFxOperationType.Extract || - operationType === DacFxOperationType.Export) + (operationType === dacFxApplication.DacFxOperationType.Deploy || + operationType === dacFxApplication.DacFxOperationType.Extract || + operationType === dacFxApplication.DacFxOperationType.Export) ) { void loadDatabases(); } @@ -151,13 +135,13 @@ export const DacFxApplicationForm = () => { const updateSuggestedPath = async () => { if ( databaseName && - (operationType === DacFxOperationType.Extract || - operationType === DacFxOperationType.Export) && + (operationType === dacFxApplication.DacFxOperationType.Extract || + operationType === dacFxApplication.DacFxOperationType.Export) && context?.extensionRpc ) { // Get the suggested full path from the controller const result = await context.extensionRpc.sendRequest( - GetSuggestedOutputPathWebviewRequest.type, + dacFxApplication.GetSuggestedOutputPathWebviewRequest.type, { databaseName, operationType, @@ -178,7 +162,7 @@ export const DacFxApplicationForm = () => { setIsConnecting(true); const result = await context?.extensionRpc?.sendRequest( - InitializeConnectionWebviewRequest.type, + dacFxApplication.InitializeConnectionWebviewRequest.type, { initialServerName, initialDatabaseName, @@ -244,7 +228,7 @@ export const DacFxApplicationForm = () => { // If not connected, connect to the server if (!selectedConnection.isConnected) { const result = await context?.extensionRpc?.sendRequest( - ConnectToServerWebviewRequest.type, + dacFxApplication.ConnectToServerWebviewRequest.type, { profileId }, ); @@ -281,7 +265,7 @@ export const DacFxApplicationForm = () => { } else { // Already connected, verify connection state and get the ownerUri const result = await context?.extensionRpc?.sendRequest( - ConnectToServerWebviewRequest.type, + dacFxApplication.ConnectToServerWebviewRequest.type, { profileId }, ); @@ -326,7 +310,7 @@ export const DacFxApplicationForm = () => { const loadDatabases = async () => { try { const result = await context?.extensionRpc?.sendRequest( - ListDatabasesWebviewRequest.type, + dacFxApplication.ListDatabasesWebviewRequest.type, { ownerUri: ownerUri || "" }, ); if (result?.databases) { @@ -358,7 +342,7 @@ export const DacFxApplicationForm = () => { try { const result = await context?.extensionRpc?.sendRequest( - ValidateFilePathWebviewRequest.type, + dacFxApplication.ValidateFilePathWebviewRequest.type, { filePath: path, shouldExist }, ); @@ -423,7 +407,7 @@ export const DacFxApplicationForm = () => { try { const result = await context?.extensionRpc?.sendRequest( - ValidateDatabaseNameWebviewRequest.type, + dacFxApplication.ValidateDatabaseNameWebviewRequest.type, { databaseName: dbName, ownerUri: ownerUri || "", @@ -456,11 +440,11 @@ export const DacFxApplicationForm = () => { // 1. User checked "New Database" but database already exists (shouldNotExist=true) // 2. User unchecked "New Database" to deploy to existing (shouldNotExist=false) if ( - operationType === DacFxOperationType.Deploy && + operationType === dacFxApplication.DacFxOperationType.Deploy && result.errorMessage === locConstants.dacFxApplication.databaseAlreadyExists ) { const confirmResult = await context?.extensionRpc?.sendRequest( - ConfirmDeployToExistingWebviewRequest.type, + dacFxApplication.ConfirmDeployToExistingWebviewRequest.type, undefined, ); @@ -500,7 +484,7 @@ export const DacFxApplicationForm = () => { let result; switch (operationType) { - case DacFxOperationType.Deploy: + case dacFxApplication.DacFxOperationType.Deploy: if ( !(await validateFilePath(filePath, true)) || !(await validateDatabaseName(databaseName, isNewDatabase)) @@ -509,7 +493,7 @@ export const DacFxApplicationForm = () => { return; } result = await context?.extensionRpc?.sendRequest( - DeployDacpacWebviewRequest.type, + dacFxApplication.DeployDacpacWebviewRequest.type, { packageFilePath: filePath, databaseName, @@ -519,7 +503,7 @@ export const DacFxApplicationForm = () => { ); break; - case DacFxOperationType.Extract: + case dacFxApplication.DacFxOperationType.Extract: if ( !(await validateFilePath(filePath, false)) || !(await validateDatabaseName(databaseName, false)) @@ -528,7 +512,7 @@ export const DacFxApplicationForm = () => { return; } result = await context?.extensionRpc?.sendRequest( - ExtractDacpacWebviewRequest.type, + dacFxApplication.ExtractDacpacWebviewRequest.type, { databaseName, packageFilePath: filePath, @@ -539,7 +523,7 @@ export const DacFxApplicationForm = () => { ); break; - case DacFxOperationType.Import: + case dacFxApplication.DacFxOperationType.Import: if ( !(await validateFilePath(filePath, true)) || !(await validateDatabaseName(databaseName, true)) @@ -548,7 +532,7 @@ export const DacFxApplicationForm = () => { return; } result = await context?.extensionRpc?.sendRequest( - ImportBacpacWebviewRequest.type, + dacFxApplication.ImportBacpacWebviewRequest.type, { packageFilePath: filePath, databaseName, @@ -557,7 +541,7 @@ export const DacFxApplicationForm = () => { ); break; - case DacFxOperationType.Export: + case dacFxApplication.DacFxOperationType.Export: if ( !(await validateFilePath(filePath, false)) || !(await validateDatabaseName(databaseName, false)) @@ -566,7 +550,7 @@ export const DacFxApplicationForm = () => { return; } result = await context?.extensionRpc?.sendRequest( - ExportBacpacWebviewRequest.type, + dacFxApplication.ExportBacpacWebviewRequest.type, { databaseName, packageFilePath: filePath, @@ -597,8 +581,8 @@ export const DacFxApplicationForm = () => { const handleBrowseFile = async () => { const fileExtension = - operationType === DacFxOperationType.Deploy || - operationType === DacFxOperationType.Extract + operationType === dacFxApplication.DacFxOperationType.Deploy || + operationType === dacFxApplication.DacFxOperationType.Extract ? "dacpac" : "bacpac"; @@ -606,9 +590,12 @@ export const DacFxApplicationForm = () => { if (requiresInputFile) { // Browse for input file (Deploy or Import) - result = await context?.extensionRpc?.sendRequest(BrowseInputFileWebviewRequest.type, { - fileExtension, - }); + result = await context?.extensionRpc?.sendRequest( + dacFxApplication.BrowseInputFileWebviewRequest.type, + { + fileExtension, + }, + ); } else { // Browse for output file (Extract or Export) // Use the suggested filename from state, or fallback to a default @@ -635,10 +622,13 @@ export const DacFxApplicationForm = () => { defaultFileName = `${databaseName || "database"}-${timestamp}.${fileExtension}`; } - result = await context?.extensionRpc?.sendRequest(BrowseOutputFileWebviewRequest.type, { - fileExtension, - defaultFileName, - }); + result = await context?.extensionRpc?.sendRequest( + dacFxApplication.BrowseOutputFileWebviewRequest.type, + { + fileExtension, + defaultFileName, + }, + ); } if (result?.filePath) { @@ -654,11 +644,11 @@ export const DacFxApplicationForm = () => { if ( requiresInputFile && context?.extensionRpc && - (operationType === DacFxOperationType.Deploy || - operationType === DacFxOperationType.Import) + (operationType === dacFxApplication.DacFxOperationType.Deploy || + operationType === dacFxApplication.DacFxOperationType.Import) ) { const nameResult = await context.extensionRpc.sendRequest( - GetSuggestedDatabaseNameWebviewRequest.type, + dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, { filePath: result.filePath, }, @@ -677,7 +667,7 @@ export const DacFxApplicationForm = () => { const handleCancel = async () => { await context?.extensionRpc?.sendNotification( - CancelDacFxApplicationWebviewNotification.type, + dacFxApplication.CancelDacFxApplicationWebviewNotification.type, ); }; @@ -692,12 +682,14 @@ export const DacFxApplicationForm = () => { }; const requiresInputFile = - operationType === DacFxOperationType.Deploy || operationType === DacFxOperationType.Import; - const showDatabaseTarget = operationType === DacFxOperationType.Deploy; + operationType === dacFxApplication.DacFxOperationType.Deploy || + operationType === dacFxApplication.DacFxOperationType.Import; + const showDatabaseTarget = operationType === dacFxApplication.DacFxOperationType.Deploy; const showDatabaseSource = - operationType === DacFxOperationType.Extract || operationType === DacFxOperationType.Export; - const showNewDatabase = operationType === DacFxOperationType.Import; - const showApplicationInfo = operationType === DacFxOperationType.Extract; + operationType === dacFxApplication.DacFxOperationType.Extract || + operationType === dacFxApplication.DacFxOperationType.Export; + const showNewDatabase = operationType === dacFxApplication.DacFxOperationType.Import; + const showApplicationInfo = operationType === dacFxApplication.DacFxOperationType.Extract; async function handleFilePathChange(value: string): Promise { setFilePath(value); From 53cd503e60e47c9532ee0c1b35926340daf937db Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:37:48 -0600 Subject: [PATCH 39/79] refactor to a base interface per pr comment --- src/sharedInterfaces/dacFxApplication.ts | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/sharedInterfaces/dacFxApplication.ts b/src/sharedInterfaces/dacFxApplication.ts index c0d948c1b4..bab4d04b8a 100644 --- a/src/sharedInterfaces/dacFxApplication.ts +++ b/src/sharedInterfaces/dacFxApplication.ts @@ -112,43 +112,43 @@ export interface DacFxApplicationWebviewState { } /** - * Parameters for deploying a DACPAC + * Base parameters for DacFx operations */ -export interface DeployDacpacParams { - packageFilePath: string; +interface DacFxOperationParams { databaseName: string; - isNewDatabase: boolean; + packageFilePath: string; ownerUri: string; } +/** + * Parameters for exporting a BACPAC + */ +export interface ExportBacpacParams extends DacFxOperationParams {} + +/** + * Parameters for deploying a DACPAC + */ +export interface DeployDacpacParams extends DacFxOperationParams { + isNewDatabase: boolean; +} + /** * Parameters for extracting a DACPAC */ -export interface ExtractDacpacParams { - databaseName: string; - packageFilePath: string; +export interface ExtractDacpacParams extends DacFxOperationParams { applicationName?: string; applicationVersion?: string; - ownerUri: string; } /** * Parameters for importing a BACPAC */ -export interface ImportBacpacParams { - packageFilePath: string; - databaseName: string; - ownerUri: string; -} +export interface ImportBacpacParams extends DacFxOperationParams {} /** * Parameters for exporting a BACPAC */ -export interface ExportBacpacParams { - databaseName: string; - packageFilePath: string; - ownerUri: string; -} +export interface ExportBacpacParams extends DacFxOperationParams {} /** * Result from a DacFx Application operation From 83bddff5ed25e8450788da232695ff98a3c1f0bf Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:40:58 -0600 Subject: [PATCH 40/79] change to expect from assert --- test/unit/mainController.test.ts | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/test/unit/mainController.test.ts b/test/unit/mainController.test.ts index 13353737bd..af1f98bd68 100644 --- a/test/unit/mainController.test.ts +++ b/test/unit/mainController.test.ts @@ -5,7 +5,7 @@ import * as sinon from "sinon"; import sinonChai from "sinon-chai"; -import { expect, assert } from "chai"; +import { expect } from "chai"; import * as chai from "chai"; import * as vscode from "vscode"; import * as Extension from "../../src/extension"; @@ -342,42 +342,27 @@ suite("MainController Tests", function () { suite("Data-Tier Application Commands", () => { test("cmdDacFxApplication command is registered", async () => { const commands = await vscode.commands.getCommands(true); - assert.ok( - commands.includes(Constants.cmdDacFxApplication), - "Expected cmdDacFxApplication to be registered", - ); + expect(commands).to.include(Constants.cmdDacFxApplication); }); test("cmdDeployDacpac command is registered", async () => { const commands = await vscode.commands.getCommands(true); - assert.ok( - commands.includes(Constants.cmdDeployDacpac), - "Expected cmdDeployDacpac to be registered", - ); + expect(commands).to.include(Constants.cmdDeployDacpac); }); test("cmdExtractDacpac command is registered", async () => { const commands = await vscode.commands.getCommands(true); - assert.ok( - commands.includes(Constants.cmdExtractDacpac), - "Expected cmdExtractDacpac to be registered", - ); + expect(commands).to.include(Constants.cmdExtractDacpac); }); test("cmdImportBacpac command is registered", async () => { const commands = await vscode.commands.getCommands(true); - assert.ok( - commands.includes(Constants.cmdImportBacpac), - "Expected cmdImportBacpac to be registered", - ); + expect(commands).to.include(Constants.cmdImportBacpac); }); test("cmdExportBacpac command is registered", async () => { const commands = await vscode.commands.getCommands(true); - assert.ok( - commands.includes(Constants.cmdExportBacpac), - "Expected cmdExportBacpac to be registered", - ); + expect(commands).to.include(Constants.cmdExportBacpac); }); }); }); From f071da3b9e4c838169d1b1e18147eb419f010955 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:46:46 -0600 Subject: [PATCH 41/79] moving require to imports for homdir --- src/controllers/dacFxApplicationWebviewController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 1828bedd50..e9cecb7aa9 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -7,6 +7,7 @@ import * as vscode from "vscode"; import * as path from "path"; import * as fs from "fs"; import { existsSync } from "fs"; +import { homedir } from "os"; import ConnectionManager from "./connectionManager"; import { DacFxService } from "../services/dacFxService"; import { IConnectionProfile } from "../models/interfaces"; @@ -192,7 +193,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri; const defaultUri = workspaceFolder ? vscode.Uri.joinPath(workspaceFolder, defaultFileName) - : vscode.Uri.file(path.join(require("os").homedir(), defaultFileName)); + : vscode.Uri.file(path.join(homedir(), defaultFileName)); const fileUri = await vscode.window.showSaveDialog({ defaultUri: defaultUri, @@ -237,7 +238,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri; const defaultUri = workspaceFolder ? vscode.Uri.joinPath(workspaceFolder, suggestedFileName) - : vscode.Uri.file(path.join(require("os").homedir(), suggestedFileName)); + : vscode.Uri.file(path.join(homedir(), suggestedFileName)); return { fullPath: defaultUri.fsPath }; }, From f8612ab581c668c40eb88483c4dbffd804741b16 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:55:13 -0600 Subject: [PATCH 42/79] localization for file dialog label --- localization/l10n/bundle.l10n.json | 3 ++- localization/xliff/vscode-mssql.xlf | 3 +++ src/constants/locConstants.ts | 1 + src/controllers/dacFxApplicationWebviewController.ts | 6 ++++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index 41888d1ea0..c1c84ae0e0 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -1894,5 +1894,6 @@ "Error loading Azure subscriptions.": "Error loading Azure subscriptions.", "Invalid connection string: {0}": "Invalid connection string: {0}", "No subscriptions available. Adjust your subscription filters to try again.": "No subscriptions available. Adjust your subscription filters to try again.", - "Error loading Azure databases.": "Error loading Azure databases." + "Error loading Azure databases.": "Error loading Azure databases.", + "Files": "Files" } diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index f34fcdb951..70068f594a 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -1636,6 +1636,9 @@ File path is required + + Files + Filter diff --git a/src/constants/locConstants.ts b/src/constants/locConstants.ts index 998fe1b602..6f7c55ed5e 100644 --- a/src/constants/locConstants.ts +++ b/src/constants/locConstants.ts @@ -2104,4 +2104,5 @@ export class DacFxApplication { public static Cancel = l10n.t("Cancel"); public static Select = l10n.t("Select"); public static Save = l10n.t("Save"); + public static Files = l10n.t("Files"); } diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index e9cecb7aa9..25720250e3 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -172,7 +172,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll canSelectMany: false, openLabel: LocConstants.DacFxApplication.Select, filters: { - [`${params.fileExtension.toUpperCase()} Files`]: [params.fileExtension], + [`${params.fileExtension.toUpperCase()} ${LocConstants.DacFxApplication.Files}`]: + [params.fileExtension], }, }); @@ -199,7 +200,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll defaultUri: defaultUri, saveLabel: LocConstants.DacFxApplication.Save, filters: { - [`${params.fileExtension.toUpperCase()} Files`]: [params.fileExtension], + [`${params.fileExtension.toUpperCase()} ${LocConstants.DacFxApplication.Files}`]: + [params.fileExtension], }, }); From 02beea3a93c6ab03ba87373b7de58653bdaea4b1 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 11:57:28 -0600 Subject: [PATCH 43/79] localization changes --- localization/l10n/bundle.l10n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index c1c84ae0e0..ff313a556c 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -1889,11 +1889,11 @@ "Database name is too long. Maximum length is 128 characters": "Database name is too long. Maximum length is 128 characters", "Database not found on the server": "Database not found on the server", "Validation failed. Please check your inputs": "Validation failed. Please check your inputs", + "Files": "Files", "Azure sign in failed.": "Azure sign in failed.", "Select subscriptions": "Select subscriptions", "Error loading Azure subscriptions.": "Error loading Azure subscriptions.", "Invalid connection string: {0}": "Invalid connection string: {0}", "No subscriptions available. Adjust your subscription filters to try again.": "No subscriptions available. Adjust your subscription filters to try again.", - "Error loading Azure databases.": "Error loading Azure databases.", - "Files": "Files" + "Error loading Azure databases.": "Error loading Azure databases." } From 76fa8a015d35b7cc204ed658c2283f4d58c1acb5 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 12:03:11 -0600 Subject: [PATCH 44/79] format changes for timestamp in the filename --- .../dacFxApplicationWebviewController.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 25720250e3..4368721bc1 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -225,15 +225,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll ? "dacpac" : "bacpac"; - // Format timestamp as yyyy-MM-dd-HH-mm - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); - const hours = String(now.getHours()).padStart(2, "0"); - const minutes = String(now.getMinutes()).padStart(2, "0"); - const timestamp = `${year}-${month}-${day}-${hours}-${minutes}`; - + const timestamp = this.formatTimestampForFilename(); const suggestedFileName = `${params.databaseName}-${timestamp}.${fileExtension}`; // Get workspace folder or home directory @@ -899,6 +891,20 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll } } + /** + * Formats the current date/time as yyyy-MM-dd-HH-mm for use in filenames + */ + private formatTimestampForFilename(): string { + const pad = (n: number) => String(n).padStart(2, "0"); + const now = new Date(); + const year = now.getFullYear(); + const month = pad(now.getMonth() + 1); + const day = pad(now.getDate()); + const hours = pad(now.getHours()); + const minutes = pad(now.getMinutes()); + return `${year}-${month}-${day}-${hours}-${minutes}`; + } + /** * Validates a database name */ From 80ee27bd45d7d5f429fa281fbcbb82b3c7469d95 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 12:17:10 -0600 Subject: [PATCH 45/79] refactor for getting the name using just one function call as suggested in pr comment --- src/controllers/dacFxApplicationWebviewController.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 4368721bc1..2f9538bb9e 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -242,12 +242,9 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll this.onRequest( dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, async (params: { filePath: string }) => { - // Extract filename without directory path - const fileName = path.basename(params.filePath); - // Remove file extension (.dacpac or .bacpac) to get the database name // Keep the full filename including any timestamps that may be present - const databaseName = fileName.replace(/\.(dacpac|bacpac)$/i, ""); + const databaseName = path.basename(params.filePath, path.extname(params.filePath)); return { databaseName }; }, From 3102d0d2d1e9387011cdbaeea024446590319131 Mon Sep 17 00:00:00 2001 From: allancascante Date: Wed, 5 Nov 2025 12:24:00 -0600 Subject: [PATCH 46/79] moved the extensions to constants as requested in pr --- .../dacFxApplicationWebviewController.ts | 6 +++++- .../dacFxApplicationWebviewController.test.ts | 18 +++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 2f9538bb9e..0e4635c34a 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -21,6 +21,10 @@ import * as dacFxApplication from "../sharedInterfaces/dacFxApplication"; import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; +// File extension constants +export const DACPAC_EXTENSION = ".dacpac"; +export const BACPAC_EXTENSION = ".bacpac"; + /** * Controller for the DacFxApplication webview. * Manages DACPAC and BACPAC operations (Deploy, Extract, Import, Export) using the Data-tier Application Framework (DacFx). @@ -489,7 +493,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll } const extension = path.extname(filePath).toLowerCase(); - if (extension !== ".dacpac" && extension !== ".bacpac") { + if (extension !== DACPAC_EXTENSION && extension !== BACPAC_EXTENSION) { return { isValid: false, errorMessage: LocConstants.DacFxApplication.InvalidFileExtension, diff --git a/test/unit/dacFxApplicationWebviewController.test.ts b/test/unit/dacFxApplicationWebviewController.test.ts index 88cbf29395..febd937050 100644 --- a/test/unit/dacFxApplicationWebviewController.test.ts +++ b/test/unit/dacFxApplicationWebviewController.test.ts @@ -9,7 +9,11 @@ import sinonChai from "sinon-chai"; import * as chai from "chai"; import { expect } from "chai"; import * as jsonRpc from "vscode-jsonrpc/node"; -import { DacFxApplicationWebviewController } from "../../src/controllers/dacFxApplicationWebviewController"; +import { + DacFxApplicationWebviewController, + DACPAC_EXTENSION, + BACPAC_EXTENSION, +} from "../../src/controllers/dacFxApplicationWebviewController"; import ConnectionManager from "../../src/controllers/connectionManager"; import { DacFxService } from "../../src/services/dacFxService"; import { @@ -565,7 +569,7 @@ suite("DacFxApplicationWebviewController", () => { expect(result.fullPath).to.include("myproject"); expect(result.fullPath).to.include("AdventureWorks"); - expect(result.fullPath).to.include(".dacpac"); + expect(result.fullPath).to.include(DACPAC_EXTENSION); // Should include timestamp in format yyyy-MM-dd-HH-mm expect(result.fullPath).to.match(/\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/); }); @@ -585,7 +589,7 @@ suite("DacFxApplicationWebviewController", () => { expect(result.fullPath).to.not.include("workspace"); expect(result.fullPath).to.include("TestDB"); - expect(result.fullPath).to.include(".bacpac"); + expect(result.fullPath).to.include(BACPAC_EXTENSION); // Should include timestamp in format yyyy-MM-dd-HH-mm expect(result.fullPath).to.match(/\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/); }); @@ -606,8 +610,8 @@ suite("DacFxApplicationWebviewController", () => { operationType: DacFxOperationType.Extract, }); - expect(result.fullPath).to.include(".dacpac"); - expect(result.fullPath).to.not.include(".bacpac"); + expect(result.fullPath).to.include(DACPAC_EXTENSION); + expect(result.fullPath).to.not.include(BACPAC_EXTENSION); }); test("get suggested output path uses correct extension for Export", async () => { @@ -626,8 +630,8 @@ suite("DacFxApplicationWebviewController", () => { operationType: DacFxOperationType.Export, }); - expect(result.fullPath).to.include(".bacpac"); - expect(result.fullPath).to.not.include(".dacpac"); + expect(result.fullPath).to.include(BACPAC_EXTENSION); + expect(result.fullPath).to.not.include(DACPAC_EXTENSION); }); test("get suggested database name removes extension from simple filename", async () => { From e0f5b32e09d933431c171c8a73d2e34f81556e56 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 09:30:21 -0600 Subject: [PATCH 47/79] simplyfing connection to server logic per PR comment --- .../dacFxApplicationWebviewController.ts | 181 +++++------------- .../DacFxApplication/dacFxApplicationForm.tsx | 87 ++------- src/sharedInterfaces/dacFxApplication.ts | 4 - 3 files changed, 66 insertions(+), 206 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 0e4635c34a..383d86332e 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -20,6 +20,7 @@ import { TelemetryViews, TelemetryActions, ActivityStatus } from "../sharedInter import * as dacFxApplication from "../sharedInterfaces/dacFxApplication"; import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; +import { getConnectionDisplayName } from "../models/connectionInfo"; // File extension constants export const DACPAC_EXTENSION = ".dacpac"; @@ -541,7 +542,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll } /** - * Lists all available connections (recent and active) + * Lists all available connections from the connection store */ private async listConnections(): Promise<{ connections: dacFxApplication.ConnectionProfile[]; @@ -549,26 +550,16 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll try { const connections: dacFxApplication.ConnectionProfile[] = []; - // Get recently used connections from connection store - const recentConnections = - this.connectionManager.connectionStore.getRecentlyUsedConnections(); + // Get all saved connections from connection store (saved profiles only, not recent connections) + const savedConnections = + await this.connectionManager.connectionStore.readAllConnections(); - // Get active connections - const activeConnections = this.connectionManager.activeConnections; - - // Build the connection profile list from recent connections - for (const conn of recentConnections) { + // Build the connection profile list from saved connections + for (const conn of savedConnections) { const profile = conn as IConnectionProfile; - const displayName = this.buildConnectionDisplayName(profile); + const displayName = getConnectionDisplayName(profile); const profileId = profile.id || `${profile.server}_${profile.database || ""}`; - // Check if this connection is active and properly connected - const ownerUri = this.connectionManager.getUriForConnection(profile); - const isConnected = - ownerUri && activeConnections[ownerUri] - ? this.connectionManager.isConnected(ownerUri) - : false; - connections.push({ displayName, server: profile.server, @@ -577,44 +568,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll profile.authenticationType, ), userName: profile.user, - isConnected, profileId, }); } - const existingProfileIds = new Set(connections.map((conn) => conn.profileId)); - - // Include active connections that may not appear in the recent list - for (const activeConnection of Object.values(activeConnections)) { - const profile = activeConnection.credentials as IConnectionProfile; - const profileId = profile.id || `${profile.server}_${profile.database || ""}`; - - if (existingProfileIds.has(profileId)) { - continue; - } - - // Only include if actually connected (not in connecting state or errored) - const ownerUri = this.connectionManager.getUriForConnection(profile); - if (!ownerUri || !this.connectionManager.isConnected(ownerUri)) { - continue; - } - - const displayName = this.buildConnectionDisplayName(profile); - - connections.push({ - displayName, - server: profile.server, - database: profile.database, - authenticationType: this.getAuthenticationTypeString( - profile.authenticationType, - ), - userName: profile.user, - isConnected: true, - profileId, - }); - existingProfileIds.add(profileId); - } - return { connections }; } catch (error) { this.logger.error(`Failed to list connections: ${error}`); @@ -639,7 +596,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll errorMessage?: string; }> { try { - // Get all connections (recent + active) + // Get all connections const { connections } = await this.listConnections(); // Helper to find matching connection @@ -691,95 +648,58 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Found a matching connection let ownerUri = params.initialOwnerUri; - let updatedConnections = connections; // Case 1: Already connected via Object Explorer (ownerUri provided) if (params.initialOwnerUri) { this.logger.verbose( `Using existing connection from Object Explorer: ${params.initialOwnerUri}`, ); - // Mark as connected if not already - if (!matchingConnection.isConnected) { - updatedConnections = connections.map((conn) => - conn.profileId === matchingConnection.profileId - ? { ...conn, isConnected: true } - : conn, - ); - } return { - connections: updatedConnections, - selectedConnection: { ...matchingConnection, isConnected: true }, + connections, + selectedConnection: matchingConnection, ownerUri: params.initialOwnerUri, autoConnected: false, // Was already connected }; } - // Case 2: Connection exists but not connected - auto-connect - if (!matchingConnection.isConnected) { - this.logger.verbose(`Auto-connecting to profile: ${matchingConnection.profileId}`); - try { - const connectResult = await this.connectToServer(matchingConnection.profileId); - - if (connectResult.isConnected && connectResult.ownerUri) { - ownerUri = connectResult.ownerUri; - updatedConnections = connections.map((conn) => - conn.profileId === matchingConnection.profileId - ? { ...conn, isConnected: true } - : conn, - ); - this.logger.info( - `Successfully auto-connected to: ${matchingConnection.server}`, - ); - return { - connections: updatedConnections, - selectedConnection: { ...matchingConnection, isConnected: true }, - ownerUri, - autoConnected: true, - }; - } else { - // Connection failed - this.logger.error( - `Auto-connect failed: ${connectResult.errorMessage || "Unknown error"}`, - ); - return { - connections, - selectedConnection: matchingConnection, - autoConnected: false, - errorMessage: connectResult.errorMessage, - }; - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - this.logger.error(`Auto-connect exception: ${errorMsg}`); + // Case 2: Try to connect to the matched profile + this.logger.verbose(`Auto-connecting to profile: ${matchingConnection.profileId}`); + try { + const connectResult = await this.connectToServer(matchingConnection.profileId); + + if (connectResult.isConnected && connectResult.ownerUri) { + ownerUri = connectResult.ownerUri; + this.logger.info( + `Successfully auto-connected to: ${matchingConnection.server}`, + ); + return { + connections, + selectedConnection: matchingConnection, + ownerUri, + autoConnected: true, + }; + } else { + // Connection failed + this.logger.error( + `Auto-connect failed: ${connectResult.errorMessage || "Unknown error"}`, + ); return { connections, selectedConnection: matchingConnection, autoConnected: false, - errorMessage: errorMsg, + errorMessage: connectResult.errorMessage, }; } - } - - // Case 3: Connection already active - fetch ownerUri - this.logger.verbose( - `Connection already active, fetching ownerUri for: ${matchingConnection.profileId}`, - ); - try { - const connectResult = await this.connectToServer(matchingConnection.profileId); - if (connectResult.ownerUri) { - ownerUri = connectResult.ownerUri; - this.logger.verbose(`Fetched ownerUri: ${ownerUri}`); - } } catch (error) { - this.logger.error(`Failed to fetch ownerUri: ${error}`); + const errorMsg = error instanceof Error ? error.message : String(error); + this.logger.error(`Auto-connect exception: ${errorMsg}`); + return { + connections, + selectedConnection: matchingConnection, + autoConnected: false, + errorMessage: errorMsg, + }; } - - return { - connections, - selectedConnection: matchingConnection, - ownerUri, - autoConnected: false, // Was already connected - }; } catch (error) { this.logger.error(`Failed to initialize connection: ${error}`); // Fallback: return empty state @@ -798,10 +718,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll profileId: string, ): Promise<{ ownerUri: string; isConnected: boolean; errorMessage?: string }> { try { - // Find the profile in recent connections - const recentConnections = - this.connectionManager.connectionStore.getRecentlyUsedConnections(); - const profile = recentConnections.find((conn: vscodeMssql.IConnectionInfo) => { + // Find the profile in saved connections + const savedConnections = + await this.connectionManager.connectionStore.readAllConnections(); + const profile = savedConnections.find((conn: vscodeMssql.IConnectionInfo) => { const connProfile = conn as IConnectionProfile; const connId = connProfile.id || `${conn.server}_${conn.database || ""}`; return connId === profileId; @@ -865,17 +785,6 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll /** * Builds a display name for a connection profile */ - private buildConnectionDisplayName(profile: IConnectionProfile): string { - let displayName = profile.profileName || profile.server; - if (profile.database) { - displayName += ` (${profile.database})`; - } - if (profile.user) { - displayName += ` - ${profile.user}`; - } - return displayName; - } - /** * Gets a string representation of the authentication type */ diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index 493b611dbd..c86d58b5a6 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -225,74 +225,29 @@ export const DacFxApplicationForm = () => { setIsConnecting(true); try { - // If not connected, connect to the server - if (!selectedConnection.isConnected) { - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ConnectToServerWebviewRequest.type, - { profileId }, - ); + // Connect to the server + const result = await context?.extensionRpc?.sendRequest( + dacFxApplication.ConnectToServerWebviewRequest.type, + { profileId }, + ); - if (result?.isConnected && result.ownerUri) { - setOwnerUri(result.ownerUri); - // Update the connection status in our list - setAvailableConnections((prev) => - prev.map((conn) => - conn.profileId === profileId ? { ...conn, isConnected: true } : conn, - ), - ); - // Databases will be loaded automatically via useEffect - } else { - // Connection failed - clear state - setOwnerUri(""); - setAvailableDatabases([]); - setDatabaseName(""); - // Ensure connection is marked as not connected - setAvailableConnections((prev) => - prev.map((conn) => - conn.profileId === profileId ? { ...conn, isConnected: false } : conn, - ), - ); - // Show error message to user - const errorMsg = - result?.errorMessage || locConstants.dacFxApplication.connectionFailed; - setValidationMessages({ - connection: { - message: errorMsg, - severity: "error", - }, - }); - } + if (result?.isConnected && result.ownerUri) { + setOwnerUri(result.ownerUri); + // Databases will be loaded automatically via useEffect } else { - // Already connected, verify connection state and get the ownerUri - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ConnectToServerWebviewRequest.type, - { profileId }, - ); - - if (result?.isConnected && result.ownerUri) { - setOwnerUri(result.ownerUri); - // Databases will be loaded automatically via useEffect - } else { - // Connection is no longer valid - clear state - setOwnerUri(""); - setAvailableDatabases([]); - setDatabaseName(""); - // Mark connection as not connected - setAvailableConnections((prev) => - prev.map((conn) => - conn.profileId === profileId ? { ...conn, isConnected: false } : conn, - ), - ); - // Show error message to user - const errorMsg = - result?.errorMessage || locConstants.dacFxApplication.connectionFailed; - setValidationMessages({ - connection: { - message: errorMsg, - severity: "error", - }, - }); - } + // Connection failed - clear state + setOwnerUri(""); + setAvailableDatabases([]); + setDatabaseName(""); + // Show error message to user + const errorMsg = + result?.errorMessage || locConstants.dacFxApplication.connectionFailed; + setValidationMessages({ + connection: { + message: errorMsg, + severity: "error", + }, + }); } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); diff --git a/src/sharedInterfaces/dacFxApplication.ts b/src/sharedInterfaces/dacFxApplication.ts index bab4d04b8a..53422bc98e 100644 --- a/src/sharedInterfaces/dacFxApplication.ts +++ b/src/sharedInterfaces/dacFxApplication.ts @@ -39,10 +39,6 @@ export interface ConnectionProfile { * User name (for SQL Auth) */ userName?: string; - /** - * Whether this connection is currently active - */ - isConnected: boolean; /** * The profile ID used to identify this connection */ From 3f7bed7be0da039bef0111b447564c36f7e345dd Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 10:38:42 -0600 Subject: [PATCH 48/79] refactor on how to pull saved connections and connecting to allow getting dbs --- .../dacFxApplicationWebviewController.ts | 79 ++- .../dacFxApplicationWebviewController.test.ts | 585 ++---------------- 2 files changed, 93 insertions(+), 571 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 383d86332e..4dd6ab27d6 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -599,43 +599,45 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get all connections const { connections } = await this.listConnections(); - // Helper to find matching connection - const findMatchingConnection = (): dacFxApplication.ConnectionProfile | undefined => { - // Priority 1: Match by profile ID if provided - if (params.initialProfileId) { - const byProfileId = connections.find( - (conn) => conn.profileId === params.initialProfileId, + let matchingConnection: dacFxApplication.ConnectionProfile | undefined; + + // Priority 1: Match by profile ID if provided + if (params.initialProfileId) { + matchingConnection = connections.find( + (conn) => conn.profileId === params.initialProfileId, + ); + if (matchingConnection) { + this.logger.verbose( + `Found connection by profile ID: ${params.initialProfileId}`, ); - if (byProfileId) { - this.logger.verbose( - `Found connection by profile ID: ${params.initialProfileId}`, - ); - return byProfileId; - } } + } - // Priority 2: Match by server name and database - if (params.initialServerName) { - const byServerAndDb = connections.find((conn) => { - const serverMatches = conn.server === params.initialServerName; - const databaseMatches = - !params.initialDatabaseName || - !conn.database || - conn.database === params.initialDatabaseName; - return serverMatches && databaseMatches; - }); - if (byServerAndDb) { + // Priority 2: Use findMatchingProfile if we have server name + if (!matchingConnection && params.initialServerName) { + // Create a temporary profile to search with + const searchProfile = { + server: params.initialServerName, + database: params.initialDatabaseName || "", + } as IConnectionProfile; + + const matchResult = + await this.connectionManager.connectionStore.findMatchingProfile(searchProfile); + + if (matchResult?.profile) { + // Find the matching connection in our list + const profileId = + matchResult.profile.id || + `${matchResult.profile.server}_${matchResult.profile.database || ""}`; + matchingConnection = connections.find((conn) => conn.profileId === profileId); + + if (matchingConnection) { this.logger.verbose( - `Found connection by server/database: ${params.initialServerName}/${params.initialDatabaseName || "default"}`, + `Found connection by server/database using findMatchingProfile: ${params.initialServerName}/${params.initialDatabaseName || "default"}`, ); - return byServerAndDb; } } - - return undefined; - }; - - const matchingConnection = findMatchingConnection(); + } if (!matchingConnection) { // No match found - return all connections, let user choose @@ -647,7 +649,6 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll } // Found a matching connection - let ownerUri = params.initialOwnerUri; // Case 1: Already connected via Object Explorer (ownerUri provided) if (params.initialOwnerUri) { @@ -662,26 +663,24 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll }; } - // Case 2: Try to connect to the matched profile - this.logger.verbose(`Auto-connecting to profile: ${matchingConnection.profileId}`); + // Case 2: Connect to the matched profile + // connectToServer handles checking if already connected internally + this.logger.verbose(`Connecting to profile: ${matchingConnection.profileId}`); try { const connectResult = await this.connectToServer(matchingConnection.profileId); if (connectResult.isConnected && connectResult.ownerUri) { - ownerUri = connectResult.ownerUri; - this.logger.info( - `Successfully auto-connected to: ${matchingConnection.server}`, - ); + this.logger.info(`Connected to: ${matchingConnection.server}`); return { connections, selectedConnection: matchingConnection, - ownerUri, + ownerUri: connectResult.ownerUri, autoConnected: true, }; } else { // Connection failed this.logger.error( - `Auto-connect failed: ${connectResult.errorMessage || "Unknown error"}`, + `Connection failed: ${connectResult.errorMessage || "Unknown error"}`, ); return { connections, @@ -692,7 +691,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - this.logger.error(`Auto-connect exception: ${errorMsg}`); + this.logger.error(`Connection exception: ${errorMsg}`); return { connections, selectedConnection: matchingConnection, diff --git a/test/unit/dacFxApplicationWebviewController.test.ts b/test/unit/dacFxApplicationWebviewController.test.ts index febd937050..fb497e416b 100644 --- a/test/unit/dacFxApplicationWebviewController.test.ts +++ b/test/unit/dacFxApplicationWebviewController.test.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- + * 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 * as sinon from "sinon"; import sinonChai from "sinon-chai"; @@ -58,9 +62,7 @@ import { TelemetryActions, ActivityStatus, } from "../../src/sharedInterfaces/telemetry"; - chai.use(sinonChai); - suite("DacFxApplicationWebviewController", () => { let sandbox: sinon.SinonSandbox; let mockContext: vscode.ExtensionContext; @@ -76,58 +78,45 @@ suite("DacFxApplicationWebviewController", () => { let panelStub: vscode.WebviewPanel; let controller: DacFxApplicationWebviewController; let fsExistsSyncStub: sinon.SinonStub; - const ownerUri = "test-connection-uri"; const initialState = { operationType: DacFxOperationType.Deploy, serverName: "test-server", }; - setup(() => { sandbox = sinon.createSandbox(); stubTelemetry(sandbox); - const loggerStub = sandbox.createStubInstance(Logger); sandbox.stub(Logger, "create").returns(loggerStub); - sandbox.stub(utils, "getNonce").returns("test-nonce"); - const connection = stubWebviewConnectionRpc(sandbox); requestHandlers = connection.requestHandlers; notificationHandlers = connection.notificationHandlers; connectionStub = connection.connection; - sandbox .stub(jsonRpc, "createMessageConnection") .returns(connectionStub as unknown as jsonRpc.MessageConnection); - panelStub = stubWebviewPanel(sandbox); createWebviewPanelStub = sandbox .stub(vscode.window, "createWebviewPanel") .callsFake(() => panelStub); - mockContext = { extensionUri: vscode.Uri.file("/tmp/ext"), extensionPath: "/tmp/ext", subscriptions: [], } as unknown as vscode.ExtensionContext; - vscodeWrapperStub = stubVscodeWrapper(sandbox); connectionManagerStub = sandbox.createStubInstance(ConnectionManager); dacFxServiceStub = sandbox.createStubInstance(DacFxService); sqlToolsClientStub = sandbox.createStubInstance(SqlToolsServiceClient); - // Set up connection manager client sandbox.stub(connectionManagerStub, "client").get(() => sqlToolsClientStub); - // Stub fs.existsSync fsExistsSyncStub = sandbox.stub(fs, "existsSync"); }); - teardown(() => { sandbox.restore(); }); - function createController(): DacFxApplicationWebviewController { controller = new DacFxApplicationWebviewController( mockContext, @@ -139,7 +128,6 @@ suite("DacFxApplicationWebviewController", () => { ); return controller; } - suite("Deployment Operations", () => { test("deploy DACPAC succeeds for new database", async () => { const mockResult: DacFxResult = { @@ -147,23 +135,18 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "operation-123", }; - dacFxServiceStub.deployDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const params = { packageFilePath: "C:\\test\\database.dacpac", databaseName: "NewDatabase", isNewDatabase: true, ownerUri: ownerUri, }; - const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); const response = (await requestHandler!(params)) as DacFxApplicationResult; - expect(dacFxServiceStub.deployDacpac).to.have.been.calledOnce; expect(dacFxServiceStub.deployDacpac).to.have.been.calledWith( params.packageFilePath, @@ -179,17 +162,14 @@ suite("DacFxApplicationWebviewController", () => { }); expect(resolveSpy).to.have.been.calledOnce; }); - test("deploy DACPAC succeeds for existing database", async () => { const mockResult: DacFxResult = { success: true, errorMessage: undefined, operationId: "operation-456", }; - dacFxServiceStub.deployDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\test\\database.dacpac", @@ -197,9 +177,7 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: false, ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(dacFxServiceStub.deployDacpac).to.have.been.calledWith( params.packageFilePath, params.databaseName, @@ -209,17 +187,14 @@ suite("DacFxApplicationWebviewController", () => { ); expect(response.success).to.be.true; }); - test("deploy DACPAC returns error on failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Deployment failed: Permission denied", operationId: "operation-789", }; - dacFxServiceStub.deployDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\test\\database.dacpac", @@ -227,19 +202,15 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: true, ownerUri: ownerUri, }; - const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); const response = await requestHandler!(params); - expect(response.success).to.be.false; expect(response.errorMessage).to.equal("Deployment failed: Permission denied"); expect(resolveSpy).to.have.been.called; }); - test("deploy DACPAC handles exception", async () => { dacFxServiceStub.deployDacpac.rejects(new Error("Network timeout")); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\test\\database.dacpac", @@ -247,14 +218,11 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: true, ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response.success).to.be.false; expect(response.errorMessage).to.equal("Network timeout"); }); }); - suite("Extract Operations", () => { test("extract DACPAC succeeds", async () => { const mockResult: DacFxResult = { @@ -262,13 +230,10 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "extract-123", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const params = { databaseName: "SourceDatabase", packageFilePath: "C:\\output\\database.dacpac", @@ -276,9 +241,7 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "1.0.0", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(dacFxServiceStub.extractDacpac).to.have.been.calledOnce; expect(dacFxServiceStub.extractDacpac).to.have.been.calledWith( params.databaseName, @@ -290,17 +253,14 @@ suite("DacFxApplicationWebviewController", () => { ); expect(response.success).to.be.true; }); - test("extract DACPAC returns error on failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Extraction failed: Database not found", operationId: "extract-456", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); const params = { databaseName: "NonExistentDatabase", @@ -309,14 +269,11 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "1.0.0", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response.success).to.be.false; expect(response.errorMessage).to.equal("Extraction failed: Database not found"); }); }); - suite("Import Operations", () => { test("import BACPAC succeeds", async () => { const mockResult: DacFxResult = { @@ -324,21 +281,16 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "import-123", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const params = { packageFilePath: "C:\\backup\\database.bacpac", databaseName: "RestoredDatabase", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(dacFxServiceStub.importBacpac).to.have.been.calledOnce; expect(dacFxServiceStub.importBacpac).to.have.been.calledWith( params.packageFilePath, @@ -348,31 +300,25 @@ suite("DacFxApplicationWebviewController", () => { ); expect(response.success).to.be.true; }); - test("import BACPAC returns error on failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Import failed: Corrupted BACPAC file", operationId: "import-456", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\backup\\corrupted.bacpac", databaseName: "TestDatabase", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response.success).to.be.false; expect(response.errorMessage).to.equal("Import failed: Corrupted BACPAC file"); }); }); - suite("Export Operations", () => { test("export BACPAC succeeds", async () => { const mockResult: DacFxResult = { @@ -380,21 +326,16 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "export-123", }; - dacFxServiceStub.exportBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const params = { databaseName: "SourceDatabase", packageFilePath: "C:\\backup\\database.bacpac", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(dacFxServiceStub.exportBacpac).to.have.been.calledOnce; expect(dacFxServiceStub.exportBacpac).to.have.been.calledWith( params.databaseName, @@ -404,161 +345,125 @@ suite("DacFxApplicationWebviewController", () => { ); expect(response.success).to.be.true; }); - test("export BACPAC returns error on failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Export failed: Insufficient permissions", operationId: "export-456", }; - dacFxServiceStub.exportBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); const params = { databaseName: "ProtectedDatabase", packageFilePath: "C:\\backup\\database.bacpac", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response.success).to.be.false; expect(response.errorMessage).to.equal("Export failed: Insufficient permissions"); }); }); - suite("Browse File Operations", () => { let showOpenDialogStub: sinon.SinonStub; let showSaveDialogStub: sinon.SinonStub; - setup(() => { showOpenDialogStub = sinon.stub(vscode.window, "showOpenDialog"); showSaveDialogStub = sinon.stub(vscode.window, "showSaveDialog"); }); - teardown(() => { showOpenDialogStub.restore(); showSaveDialogStub.restore(); }); - test("browse input file returns file path when file is selected", async () => { const mockUri = vscode.Uri.file("C:\\test\\database.dacpac"); showOpenDialogStub.resolves([mockUri]); createController(); - const requestHandler = requestHandlers.get(BrowseInputFileWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const response = await requestHandler!({ fileExtension: "dacpac" }); - expect(showOpenDialogStub).to.have.been.calledOnce; expect(response.filePath).to.equal(mockUri.fsPath); }); - test("browse input file returns undefined when no file is selected", async () => { showOpenDialogStub.resolves(undefined); createController(); - const requestHandler = requestHandlers.get(BrowseInputFileWebviewRequest.type.method); const response = await requestHandler!({ fileExtension: "dacpac" }); - expect(showOpenDialogStub).to.have.been.calledOnce; expect(response.filePath).to.be.undefined; }); - test("browse input file returns undefined when empty array is returned", async () => { showOpenDialogStub.resolves([]); createController(); - const requestHandler = requestHandlers.get(BrowseInputFileWebviewRequest.type.method); const response = await requestHandler!({ fileExtension: "bacpac" }); - expect(showOpenDialogStub).to.have.been.calledOnce; expect(response.filePath).to.be.undefined; }); - test("browse output file returns file path when file is selected", async () => { const mockUri = vscode.Uri.file("C:\\output\\database.dacpac"); showSaveDialogStub.resolves(mockUri); createController(); - const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const response = await requestHandler!({ fileExtension: "dacpac", defaultFileName: "database.dacpac", }); - expect(showSaveDialogStub).to.have.been.calledOnce; expect(response.filePath).to.equal(mockUri.fsPath); }); - test("browse output file returns undefined when dialog is cancelled", async () => { showSaveDialogStub.resolves(undefined); createController(); - const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); const response = await requestHandler!({ fileExtension: "bacpac", }); - expect(showSaveDialogStub).to.have.been.calledOnce; expect(response.filePath).to.be.undefined; }); - test("browse output file uses workspace folder as default location when available", async () => { const workspaceFolder = vscode.Uri.file("/workspace/myproject"); sandbox .stub(vscode.workspace, "workspaceFolders") .get(() => [{ uri: workspaceFolder, name: "myproject", index: 0 }]); - const mockUri = vscode.Uri.file("/workspace/myproject/database.dacpac"); showSaveDialogStub.resolves(mockUri); createController(); - const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); await requestHandler!({ fileExtension: "dacpac", defaultFileName: "database.dacpac", }); - expect(showSaveDialogStub).to.have.been.calledOnce; const options = showSaveDialogStub.firstCall.args[0]; expect(options.defaultUri.fsPath).to.include("myproject"); expect(options.defaultUri.fsPath).to.include("database.dacpac"); }); - test("browse output file falls back to home directory when no workspace folder available", async () => { sandbox.stub(vscode.workspace, "workspaceFolders").get(() => undefined); - const mockUri = vscode.Uri.file("/home/user/database.bacpac"); showSaveDialogStub.resolves(mockUri); createController(); - const requestHandler = requestHandlers.get(BrowseOutputFileWebviewRequest.type.method); await requestHandler!({ fileExtension: "bacpac", defaultFileName: "database.bacpac", }); - expect(showSaveDialogStub).to.have.been.calledOnce; const options = showSaveDialogStub.firstCall.args[0]; // Should use home directory - verify it doesn't contain workspace path expect(options.defaultUri.fsPath).to.not.include("workspace"); expect(options.defaultUri.fsPath).to.include("database.bacpac"); }); - test("get suggested output path generates full path with workspace folder", async () => { const workspaceFolder = vscode.Uri.file("/workspace/myproject"); sandbox .stub(vscode.workspace, "workspaceFolders") .get(() => [{ uri: workspaceFolder, name: "myproject", index: 0 }]); - createController(); - const requestHandler = requestHandlers.get( GetSuggestedOutputPathWebviewRequest.type.method, ); @@ -566,19 +471,15 @@ suite("DacFxApplicationWebviewController", () => { databaseName: "AdventureWorks", operationType: DacFxOperationType.Extract, }); - expect(result.fullPath).to.include("myproject"); expect(result.fullPath).to.include("AdventureWorks"); expect(result.fullPath).to.include(DACPAC_EXTENSION); // Should include timestamp in format yyyy-MM-dd-HH-mm expect(result.fullPath).to.match(/\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/); }); - test("get suggested output path falls back to home directory when no workspace", async () => { sandbox.stub(vscode.workspace, "workspaceFolders").get(() => undefined); - createController(); - const requestHandler = requestHandlers.get( GetSuggestedOutputPathWebviewRequest.type.method, ); @@ -586,22 +487,18 @@ suite("DacFxApplicationWebviewController", () => { databaseName: "TestDB", operationType: DacFxOperationType.Export, }); - expect(result.fullPath).to.not.include("workspace"); expect(result.fullPath).to.include("TestDB"); expect(result.fullPath).to.include(BACPAC_EXTENSION); // Should include timestamp in format yyyy-MM-dd-HH-mm expect(result.fullPath).to.match(/\d{4}-\d{2}-\d{2}-\d{2}-\d{2}/); }); - test("get suggested output path uses correct extension for Extract", async () => { const workspaceFolder = vscode.Uri.file("/workspace/test"); sandbox .stub(vscode.workspace, "workspaceFolders") .get(() => [{ uri: workspaceFolder, name: "test", index: 0 }]); - createController(); - const requestHandler = requestHandlers.get( GetSuggestedOutputPathWebviewRequest.type.method, ); @@ -609,19 +506,15 @@ suite("DacFxApplicationWebviewController", () => { databaseName: "MyDB", operationType: DacFxOperationType.Extract, }); - expect(result.fullPath).to.include(DACPAC_EXTENSION); expect(result.fullPath).to.not.include(BACPAC_EXTENSION); }); - test("get suggested output path uses correct extension for Export", async () => { const workspaceFolder = vscode.Uri.file("/workspace/test"); sandbox .stub(vscode.workspace, "workspaceFolders") .get(() => [{ uri: workspaceFolder, name: "test", index: 0 }]); - createController(); - const requestHandler = requestHandlers.get( GetSuggestedOutputPathWebviewRequest.type.method, ); @@ -629,14 +522,11 @@ suite("DacFxApplicationWebviewController", () => { databaseName: "MyDB", operationType: DacFxOperationType.Export, }); - expect(result.fullPath).to.include(BACPAC_EXTENSION); expect(result.fullPath).to.not.include(DACPAC_EXTENSION); }); - test("get suggested database name removes extension from simple filename", async () => { createController(); - const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); @@ -647,13 +537,10 @@ suite("DacFxApplicationWebviewController", () => { const result = await requestHandler!({ filePath: testPath, }); - expect(result.databaseName).to.equal("AdventureWorks"); }); - test("get suggested database name keeps timestamp in filename", async () => { createController(); - const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); @@ -664,13 +551,10 @@ suite("DacFxApplicationWebviewController", () => { const result = await requestHandler!({ filePath: testPath, }); - expect(result.databaseName).to.equal("AdventureWorks-2025-10-31-14-30"); }); - test("get suggested database name works with .bacpac extension", async () => { createController(); - const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); @@ -681,13 +565,10 @@ suite("DacFxApplicationWebviewController", () => { const result = await requestHandler!({ filePath: testPath, }); - expect(result.databaseName).to.equal("MyDatabase"); }); - test("get suggested database name handles complex filename with multiple hyphens", async () => { createController(); - const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); @@ -698,13 +579,10 @@ suite("DacFxApplicationWebviewController", () => { const result = await requestHandler!({ filePath: testPath, }); - expect(result.databaseName).to.equal("My-Complex-Database-Name-2025-01-15-10-30"); }); - test("get suggested database name extracts only filename from full path", async () => { createController(); - const requestHandler = requestHandlers.get( GetSuggestedDatabaseNameWebviewRequest.type.method, ); @@ -715,13 +593,11 @@ suite("DacFxApplicationWebviewController", () => { const result = await requestHandler!({ filePath: testPath, }); - expect(result.databaseName).to.equal("TestDB"); expect(result.databaseName).to.not.include("path"); expect(result.databaseName).to.not.include("files"); }); }); - suite("Operation Response Properties", () => { test("extract DACPAC response includes all required properties", async () => { const mockResult: DacFxResult = { @@ -729,10 +605,8 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "extract-op-123", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); const params = { databaseName: "TestDB", @@ -741,9 +615,7 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "1.0.0", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response).to.have.property("success"); expect(response).to.have.property("errorMessage"); expect(response).to.have.property("operationId"); @@ -751,17 +623,14 @@ suite("DacFxApplicationWebviewController", () => { expect(response.errorMessage).to.be.undefined; expect(response.operationId).to.equal("extract-op-123"); }); - test("extract DACPAC response includes error details on failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Database extraction failed", operationId: "extract-op-456", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); const params = { databaseName: "TestDB", @@ -770,9 +639,7 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "1.0.0", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response).to.have.property("success"); expect(response).to.have.property("errorMessage"); expect(response).to.have.property("operationId"); @@ -780,26 +647,21 @@ suite("DacFxApplicationWebviewController", () => { expect(response.errorMessage).to.equal("Database extraction failed"); expect(response.operationId).to.equal("extract-op-456"); }); - test("import BACPAC response includes all required properties", async () => { const mockResult: DacFxResult = { success: true, errorMessage: undefined, operationId: "import-op-789", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\backup\\test.bacpac", databaseName: "RestoredDB", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response).to.have.property("success"); expect(response).to.have.property("errorMessage"); expect(response).to.have.property("operationId"); @@ -807,26 +669,21 @@ suite("DacFxApplicationWebviewController", () => { expect(response.errorMessage).to.be.undefined; expect(response.operationId).to.equal("import-op-789"); }); - test("import BACPAC response includes error details on failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "BACPAC file is corrupted", operationId: "import-op-101", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\backup\\corrupted.bacpac", databaseName: "TestDB", ownerUri: ownerUri, }; - const response = await requestHandler!(params); - expect(response).to.have.property("success"); expect(response).to.have.property("errorMessage"); expect(response).to.have.property("operationId"); @@ -835,168 +692,131 @@ suite("DacFxApplicationWebviewController", () => { expect(response.operationId).to.equal("import-op-101"); }); }); - suite("File Path Validation", () => { test("validates existing DACPAC file", async () => { fsExistsSyncStub.returns(true); createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const response = await requestHandler!({ filePath: "C:\\test\\database.dacpac", shouldExist: true, }); - expect(response.isValid).to.be.true; expect(response.errorMessage).to.be.undefined; }); - test("rejects non-existent file when it should exist", async () => { fsExistsSyncStub.returns(false); createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ filePath: "C:\\test\\missing.dacpac", shouldExist: true, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.FileNotFound); }); - test("rejects empty file path", async () => { createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ filePath: "", shouldExist: true, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.FilePathRequired); }); - test("rejects invalid file extension", async () => { fsExistsSyncStub.returns(true); createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ filePath: "C:\\test\\database.txt", shouldExist: true, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( LocConstants.DacFxApplication.InvalidFileExtension, ); }); - test("validates output file path when directory exists", async () => { // File doesn't exist, but directory does const testDir = path.join( path.sep === "\\" ? "C:\\database-test-folder" : "/database-test-folder", ); const testFile = path.join(testDir, "database.dacpac"); - fsExistsSyncStub.withArgs(testFile).returns(false); fsExistsSyncStub.withArgs(testDir).returns(true); createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ filePath: testFile, shouldExist: false, }); - expect(response.isValid).to.be.true; expect(response.errorMessage).to.be.undefined; }); - test("warns when output file already exists", async () => { // Both file and directory exist fsExistsSyncStub.returns(true); createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ filePath: "C:\\output\\existing.dacpac", shouldExist: false, }); - expect(response.isValid).to.be.true; expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.FileAlreadyExists); }); - test("rejects output file path when directory doesn't exist", async () => { fsExistsSyncStub.returns(false); createController(); - const requestHandler = requestHandlers.get(ValidateFilePathWebviewRequest.type.method); const response = await requestHandler!({ filePath: "C:\\nonexistent\\database.dacpac", shouldExist: false, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.DirectoryNotFound); }); }); - suite("Database Operations", () => { test("lists databases successfully", async () => { const mockDatabases = { databaseNames: ["master", "tempdb", "model", "msdb", "TestDB"], }; - sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .resolves(mockDatabases); - createController(); - const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); - const response = await requestHandler!({ ownerUri: ownerUri }); - expect(response.databases).to.deep.equal(mockDatabases.databaseNames); expect(sqlToolsClientStub.sendRequest).to.have.been.calledWith( ListDatabasesRequest.type, { ownerUri: ownerUri }, ); }); - test("returns empty array when list databases fails", async () => { sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .rejects(new Error("Connection failed")); - createController(); - const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); const response = await requestHandler!({ ownerUri: ownerUri }); - expect(response.databases).to.be.an("array").that.is.empty; }); }); - suite("Database Name Validation", () => { test("validates non-existent database name for new database", async () => { const mockDatabases = { databaseNames: ["master", "tempdb", "model", "msdb"], }; - sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .resolves(mockDatabases); - createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1005,21 +825,16 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.true; }); - test("allows existing database name for new database with warning", async () => { const mockDatabases = { databaseNames: ["master", "tempdb", "ExistingDB"], }; - sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .resolves(mockDatabases); - createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1028,24 +843,19 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.true; expect(response.errorMessage).to.equal( LocConstants.DacFxApplication.DatabaseAlreadyExists, ); }); - test("validates existing database name for extract/export", async () => { const mockDatabases = { databaseNames: ["master", "tempdb", "SourceDB"], }; - sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .resolves(mockDatabases); - createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1054,21 +864,16 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: false, }); - expect(response.isValid).to.be.true; }); - test("rejects non-existent database name for extract/export", async () => { const mockDatabases = { databaseNames: ["master", "tempdb"], }; - sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .resolves(mockDatabases); - createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1077,14 +882,11 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: false, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal(LocConstants.DacFxApplication.DatabaseNotFound); }); - test("rejects empty database name", async () => { createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1093,16 +895,13 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( LocConstants.DacFxApplication.DatabaseNameRequired, ); }); - test("rejects database name with invalid characters", async () => { createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1111,16 +910,13 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( LocConstants.DacFxApplication.InvalidDatabaseName, ); }); - test("rejects database name that is too long", async () => { createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1130,24 +926,19 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.false; expect(response.errorMessage).to.equal( LocConstants.DacFxApplication.DatabaseNameTooLong, ); }); - test("validates database name case-insensitively with warning", async () => { const mockDatabases = { databaseNames: ["ExistingDB"], }; - sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .resolves(mockDatabases); - createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1156,20 +947,16 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.true; expect(response.errorMessage).to.equal( LocConstants.DacFxApplication.DatabaseAlreadyExists, ); }); - test("returns validation failed on error", async () => { sqlToolsClientStub.sendRequest .withArgs(ListDatabasesRequest.type, sinon.match.any) .rejects(new Error("Network error")); - createController(); - const requestHandler = requestHandlers.get( ValidateDatabaseNameWebviewRequest.type.method, ); @@ -1178,51 +965,40 @@ suite("DacFxApplicationWebviewController", () => { ownerUri: ownerUri, shouldNotExist: true, }); - expect(response.isValid).to.be.false; // Now returns actual error message instead of generic one expect(response.errorMessage).to.include("Failed to validate database name"); expect(response.errorMessage).to.include("Network error"); }); }); - suite("Cancel Operation", () => { test("cancel notification resolves dialog with undefined and disposes panel", async () => { createController(); - const cancelHandler = notificationHandlers.get( CancelDacFxApplicationWebviewNotification.type.method, ); expect(cancelHandler, "Cancel handler was not registered").to.be.a("function"); - const resultPromise = controller.dialogResult.promise; const resolveSpy = sandbox.spy(controller.dialogResult, "resolve"); - (cancelHandler as () => void)(); const resolvedValue = await resultPromise; - expect(resolveSpy).to.have.been.calledOnceWithExactly(undefined); expect(panelStub.dispose).to.have.been.calledOnce; expect(resolvedValue).to.be.undefined; }); }); - suite("Deploy to Existing Database Confirmation", () => { test("confirmation dialog shows and returns confirmed=true when user clicks Deploy", async () => { createController(); - const confirmHandler = requestHandlers.get( ConfirmDeployToExistingWebviewRequest.type.method, ); expect(confirmHandler, "Confirm handler was not registered").to.be.a("function"); - // Mock user clicking "Deploy" button vscodeWrapperStub.showWarningMessageAdvanced.resolves( LocConstants.DacFxApplication.DeployToExistingConfirm, ); - const response = await confirmHandler!(undefined); - expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, @@ -1230,20 +1006,15 @@ suite("DacFxApplicationWebviewController", () => { ); expect(response.confirmed).to.be.true; }); - test("confirmation dialog returns confirmed=false when user clicks Cancel", async () => { createController(); - const confirmHandler = requestHandlers.get( ConfirmDeployToExistingWebviewRequest.type.method, ); expect(confirmHandler, "Confirm handler was not registered").to.be.a("function"); - // Mock user clicking Cancel button (VS Code automatically adds this) vscodeWrapperStub.showWarningMessageAdvanced.resolves(undefined); - const response = await confirmHandler!(undefined); - expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, @@ -1251,20 +1022,15 @@ suite("DacFxApplicationWebviewController", () => { ); expect(response.confirmed).to.be.false; }); - test("confirmation dialog returns confirmed=false when user dismisses dialog (ESC)", async () => { createController(); - const confirmHandler = requestHandlers.get( ConfirmDeployToExistingWebviewRequest.type.method, ); expect(confirmHandler, "Confirm handler was not registered").to.be.a("function"); - // Mock user dismissing dialog with ESC (returns undefined) vscodeWrapperStub.showWarningMessageAdvanced.resolves(undefined); - const response = await confirmHandler!(undefined); - expect(vscodeWrapperStub.showWarningMessageAdvanced).to.have.been.calledOnceWith( LocConstants.DacFxApplication.DeployToExistingMessage, { modal: true }, @@ -1273,11 +1039,9 @@ suite("DacFxApplicationWebviewController", () => { expect(response.confirmed).to.be.false; }); }); - suite("Controller Initialization", () => { test("creates webview panel with correct configuration", () => { createController(); - expect(createWebviewPanelStub).to.have.been.calledOnce; expect(createWebviewPanelStub).to.have.been.calledWith( "mssql-react-webview", @@ -1286,10 +1050,8 @@ suite("DacFxApplicationWebviewController", () => { sinon.match.any, ); }); - test("registers all request handlers", () => { createController(); - expect(requestHandlers.has(DeployDacpacWebviewRequest.type.method)).to.be.true; expect(requestHandlers.has(ExtractDacpacWebviewRequest.type.method)).to.be.true; expect(requestHandlers.has(ImportBacpacWebviewRequest.type.method)).to.be.true; @@ -1302,30 +1064,23 @@ suite("DacFxApplicationWebviewController", () => { expect(requestHandlers.has(ConfirmDeployToExistingWebviewRequest.type.method)).to.be .true; }); - test("registers cancel notification handler", () => { createController(); - expect(notificationHandlers.has(CancelDacFxApplicationWebviewNotification.type.method)) .to.be.true; }); - test("returns correct owner URI", () => { createController(); - expect(controller.ownerUri).to.equal(ownerUri); }); }); - suite("Connection Operations", () => { let connectionStoreStub: sinon.SinonStubbedInstance; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockConnections: any[]; - setup(() => { connectionStoreStub = sandbox.createStubInstance(ConnectionStore); sandbox.stub(connectionManagerStub, "connectionStore").get(() => connectionStoreStub); - // Create mock connection profiles mockConnections = [ { @@ -1354,10 +1109,8 @@ suite("DacFxApplicationWebviewController", () => { }, ]; }); - test("lists connections successfully", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); - + connectionStoreStub.readAllConnections.resolves(mockConnections); // Mock active connections - conn1 is connected const mockActiveConnections = { uri1: { @@ -1370,7 +1123,6 @@ suite("DacFxApplicationWebviewController", () => { sandbox .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); - // Stub getUriForConnection and isConnected for the test connectionManagerStub.getUriForConnection.callsFake((profile) => { if ( @@ -1382,71 +1134,48 @@ suite("DacFxApplicationWebviewController", () => { return undefined; }); connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({}); - expect(result).to.exist; - expect(result.connections).to.have.lengthOf(4); - + expect(result.connections).to.have.lengthOf(3); // Verify first connection const conn1 = result.connections[0]; expect(conn1.server).to.equal("server1.database.windows.net"); expect(conn1.database).to.equal("db1"); expect(conn1.userName).to.equal("admin"); expect(conn1.authenticationType).to.equal("SQL Login"); - expect(conn1.isConnected).to.be.true; expect(conn1.profileId).to.equal("conn1"); expect(conn1.displayName).to.include("Server 1 - db1"); - // Verify second connection const conn2 = result.connections[1]; expect(conn2.server).to.equal("localhost"); expect(conn2.authenticationType).to.equal("Integrated"); - expect(conn2.isConnected).to.be.false; - // Verify third connection const conn3 = result.connections[2]; expect(conn3.server).to.equal("server2.database.windows.net"); expect(conn3.authenticationType).to.equal("Azure MFA"); - expect(conn3.isConnected).to.be.false; }); - - test("returns empty array when getRecentlyUsedConnections fails", async () => { - connectionStoreStub.getRecentlyUsedConnections.throws( - new Error("Connection store error"), - ); - + test("returns empty array when readAllConnections fails", async () => { + connectionStoreStub.readAllConnections.rejects(new Error("Connection store error")); createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({}); - expect(result).to.exist; expect(result.connections).to.be.an("array").that.is.empty; }); - test("builds display name correctly with all fields", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - const conn = result.connections[0]; - expect(conn.displayName).to.include("Server 1 - db1"); - expect(conn.displayName).to.include("(db1)"); - expect(conn.displayName).to.include("admin"); + // getConnectionDisplayName returns profileName if available + expect(conn.displayName).to.equal("Server 1 - db1"); }); - test("builds display name without optional fields", async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const minimalConnection: any = { @@ -1457,50 +1186,39 @@ suite("DacFxApplicationWebviewController", () => { id: "conn-minimal", authenticationType: 1, }; - - connectionStoreStub.getRecentlyUsedConnections.returns([minimalConnection]); + connectionStoreStub.readAllConnections.resolves([minimalConnection]); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - const conn = result.connections[0]; - expect(conn.displayName).to.equal("testserver"); + // When profileName is not set, getConnectionDisplayName generates: server, database (authType) + // With authenticationType=1 (Integrated), database undefined -> "testserver, (1)" + expect(conn.displayName).to.include("testserver"); + expect(conn.displayName).to.include(""); }); - test("connects to server successfully when not already connected", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); connectionManagerStub.getUriForConnection.returns("new-owner-uri"); connectionManagerStub.connect.resolves(true); - // No active connections initially sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({ profileId: "conn1" }); - expect(result).to.exist; - expect(result.isConnected).to.be.true; expect(result.ownerUri).to.equal("new-owner-uri"); expect(result.errorMessage).to.be.undefined; - // Called twice: once to check if connected, once after connecting to get the URI expect(connectionManagerStub.getUriForConnection).to.have.been.calledTwice; expect(connectionManagerStub.connect).to.have.been.calledOnce; }); - test("retrieves ownerUri after successful connection when initially undefined", async () => { // This test validates the bug fix where getUriForConnection returns undefined // before connection (since connection doesn't exist yet), but after connect() // succeeds, we call getUriForConnection again to get the actual URI - connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); - + connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); // First call returns undefined (connection doesn't exist yet) // Second call returns the actual URI (after connection is established) connectionManagerStub.getUriForConnection @@ -1508,36 +1226,26 @@ suite("DacFxApplicationWebviewController", () => { .returns(undefined) .onSecondCall() .returns("generated-owner-uri-123"); - connectionManagerStub.connect.resolves(true); - // No active connections initially sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({ profileId: "conn1" }); - expect(result).to.exist; - expect(result.isConnected).to.be.true; expect(result.ownerUri).to.equal("generated-owner-uri-123"); expect(result.errorMessage).to.be.undefined; - // Verify the sequence of calls expect(connectionManagerStub.getUriForConnection).to.have.been.calledTwice; expect(connectionManagerStub.connect).to.have.been.calledOnce; // connect() should be called with empty string to let it generate the URI expect(connectionManagerStub.connect).to.have.been.calledWith("", mockConnections[0]); }); - test("returns existing ownerUri when already connected", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); connectionManagerStub.getUriForConnection.returns("existing-owner-uri"); connectionManagerStub.isConnected.withArgs("existing-owner-uri").returns(true); - // Mock that connection already exists const mockActiveConnections = { "existing-owner-uri": { @@ -1550,72 +1258,48 @@ suite("DacFxApplicationWebviewController", () => { sandbox .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); - createController(); - const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); const result = await handler!({ profileId: "conn1" }); - - expect(result.isConnected).to.be.true; expect(result.ownerUri).to.equal("existing-owner-uri"); expect(result.errorMessage).to.be.undefined; - // Should not call connect since already connected expect(connectionManagerStub.connect).to.not.have.been.called; }); - test("returns error when profile not found", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + connectionStoreStub.readAllConnections.resolves(mockConnections); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); const result = await handler!({ profileId: "non-existent-id" }); - - expect(result.isConnected).to.be.false; expect(result.ownerUri).to.equal(""); expect(result.errorMessage).to.equal("Connection profile not found"); }); - test("returns error when connection fails", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); connectionManagerStub.getUriForConnection.returns("new-owner-uri"); connectionManagerStub.connect.resolves(false); // Connection failed - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); const result = await handler!({ profileId: "conn1" }); - - expect(result.isConnected).to.be.false; expect(result.ownerUri).to.equal(""); expect(result.errorMessage).to.equal("Failed to connect to server"); }); - test("handles connection exception gracefully", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns([mockConnections[0]]); + connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); connectionManagerStub.getUriForConnection.returns("new-owner-uri"); connectionManagerStub.connect.rejects(new Error("Network timeout")); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); const result = await handler!({ profileId: "conn1" }); - - expect(result.isConnected).to.be.false; expect(result.ownerUri).to.equal(""); expect(result.errorMessage).to.include("Connection failed"); expect(result.errorMessage).to.include("Network timeout"); }); - test("identifies connected server by matching server and database", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); - + connectionStoreStub.readAllConnections.resolves(mockConnections); // Mock active connection with matching server and database const mockActiveConnections = { uri1: { @@ -1628,7 +1312,6 @@ suite("DacFxApplicationWebviewController", () => { sandbox .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); - // Stub getUriForConnection and isConnected connectionManagerStub.getUriForConnection.callsFake((profile) => { if (profile.server === "localhost" && profile.database === "master") { @@ -1637,25 +1320,19 @@ suite("DacFxApplicationWebviewController", () => { return undefined; }); connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - // Find localhost connection const localhostConn = result.connections.find((c) => c.server === "localhost"); expect(localhostConn).to.exist; - expect(localhostConn!.isConnected).to.be.true; }); - test("identifies connected server when database is undefined in both", async () => { const connectionWithoutDb = { ...mockConnections[2], database: undefined, }; - connectionStoreStub.getRecentlyUsedConnections.returns([connectionWithoutDb]); - + connectionStoreStub.readAllConnections.resolves([connectionWithoutDb]); // Mock active connection without database const mockActiveConnections = { uri1: { @@ -1668,7 +1345,6 @@ suite("DacFxApplicationWebviewController", () => { sandbox .stub(connectionManagerStub, "activeConnections") .get(() => mockActiveConnections); - // Stub getUriForConnection and isConnected connectionManagerStub.getUriForConnection.callsFake((profile) => { if (profile.server === "server2.database.windows.net") { @@ -1677,172 +1353,126 @@ suite("DacFxApplicationWebviewController", () => { return undefined; }); connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - - expect(result.connections[0].isConnected).to.be.true; }); - test("generates profileId from server and database when id is missing", async () => { const connectionWithoutId: (typeof mockConnections)[0] = { ...mockConnections[0], id: undefined, }; - connectionStoreStub.getRecentlyUsedConnections.returns([connectionWithoutId]); + connectionStoreStub.readAllConnections.resolves([connectionWithoutId]); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - expect(result.connections[0].profileId).to.equal("server1.database.windows.net_db1"); }); - test("matches connection by server and database when both provided", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + connectionStoreStub.readAllConnections.resolves(mockConnections); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - // Find the connection that matches server1.database.windows.net and db1 const matchingConnection = result.connections.find( (conn) => conn.server === "server1.database.windows.net" && conn.database === "db1", ); - expect(matchingConnection).to.exist; expect(matchingConnection!.profileId).to.equal("conn1"); }); - test("matches connection by server only when database is not specified", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + connectionStoreStub.readAllConnections.resolves(mockConnections); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - // Find the connection that matches localhost (conn2 has master database) const matchingConnection = result.connections.find( (conn) => conn.server === "localhost" && conn.database === "master", ); - expect(matchingConnection).to.exist; expect(matchingConnection!.profileId).to.equal("conn2"); }); - test("finds connection when database is undefined in profile", async () => { // This tests the scenario where a server-level connection exists // (database is undefined in the connection profile) - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + connectionStoreStub.readAllConnections.resolves(mockConnections); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - // conn3 has undefined database - should still be findable by server const matchingConnection = result.connections.find( (conn) => conn.server === "server2.database.windows.net", ); - expect(matchingConnection).to.exist; expect(matchingConnection!.profileId).to.equal("conn3"); expect(matchingConnection!.database).to.be.undefined; }); - test("connection matching is case-sensitive for server names", async () => { - connectionStoreStub.getRecentlyUsedConnections.returns(mockConnections); + connectionStoreStub.readAllConnections.resolves(mockConnections); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - // Case must match exactly const matchingConnection = result.connections.find( (conn) => conn.server === "LOCALHOST", // Different case ); - expect(matchingConnection).to.be.undefined; - // Correct case should work const correctMatch = result.connections.find((conn) => conn.server === "localhost"); expect(correctMatch).to.exist; }); }); - suite("Database Operations with Empty OwnerUri", () => { test("returns empty array when ownerUri is empty for list databases", async () => { createController(); - const handler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({ ownerUri: "" }); - expect(result).to.exist; expect(result.databases).to.be.an("array").that.is.empty; }); - test("returns empty array when ownerUri is whitespace for list databases", async () => { createController(); - const handler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); const result = await handler!({ ownerUri: " " }); - expect(result.databases).to.be.an("array").that.is.empty; }); - test("returns validation error when ownerUri is empty for database name validation", async () => { createController(); - const handler = requestHandlers.get(ValidateDatabaseNameWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({ databaseName: "TestDB", ownerUri: "", shouldNotExist: true, }); - expect(result).to.exist; expect(result.isValid).to.be.false; expect(result.errorMessage).to.include("No active connection"); }); - test("returns validation error when ownerUri is whitespace for database name validation", async () => { createController(); - const handler = requestHandlers.get(ValidateDatabaseNameWebviewRequest.type.method); const result = await handler!({ databaseName: "TestDB", ownerUri: " ", shouldNotExist: true, }); - expect(result.isValid).to.be.false; expect(result.errorMessage).to.include("No active connection"); }); }); - suite("Initialize Connection", () => { let connStoreStub: sinon.SinonStubbedInstance; - setup(() => { connStoreStub = sandbox.createStubInstance(ConnectionStore); sandbox.stub(connectionManagerStub, "connectionStore").get(() => connStoreStub); }); - test("returns all connections when no initial state provided", async () => { // Setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1855,25 +1485,19 @@ suite("DacFxApplicationWebviewController", () => { user: "sa", }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); expect(handler).to.exist; - const result = await handler!({}); - // Verify expect(result).to.exist; expect(result.connections).to.have.lengthOf(1); expect(result.autoConnected).to.be.false; expect(result.selectedConnection).to.be.undefined; }); - test("matches and returns connection by profile ID", async () => { // Setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1890,24 +1514,19 @@ suite("DacFxApplicationWebviewController", () => { authenticationType: 1, // Integrated }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialProfileId: "profile2", }); - // Verify expect(result.selectedConnection).to.exist; expect(result.selectedConnection?.server).to.equal("server2"); expect(result.autoConnected).to.be.false; }); - test("matches connection by server and database names", async () => { // Setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1925,25 +1544,25 @@ suite("DacFxApplicationWebviewController", () => { authenticationType: 1, // Integrated }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); + // Stub findMatchingProfile to return the matching connection + connStoreStub.findMatchingProfile.resolves({ + profile: recentConns[1], + score: 2, // ServerAndDatabase match + }); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialServerName: "testserver", initialDatabaseName: "testdb", }); - // Verify expect(result.selectedConnection).to.exist; expect(result.selectedConnection?.server).to.equal("testserver"); expect(result.selectedConnection?.database).to.equal("testdb"); }); - test("uses existing ownerUri when provided from Object Explorer", async () => { // Setup const testOwnerUri = "test-owner-uri"; @@ -1956,25 +1575,25 @@ suite("DacFxApplicationWebviewController", () => { authenticationType: 2, // SqlLogin }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); + // Stub findMatchingProfile to return the matching connection + connStoreStub.findMatchingProfile.resolves({ + profile: recentConns[0], + score: 2, // ServerAndDatabase match + }); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialServerName: "server1", initialOwnerUri: testOwnerUri, }); - // Verify expect(result.selectedConnection).to.exist; expect(result.ownerUri).to.equal(testOwnerUri); expect(result.autoConnected).to.be.false; // Was already connected }); - test("auto-connects when matching connection is not active", async () => { // Setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1987,27 +1606,22 @@ suite("DacFxApplicationWebviewController", () => { user: "sa", }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); connectionManagerStub.getUriForConnection.returns("new-owner-uri"); connectionManagerStub.connect.resolves(true); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialProfileId: "profile1", }); - // Verify expect(result.selectedConnection).to.exist; expect(result.autoConnected).to.be.true; expect(result.ownerUri).to.equal("new-owner-uri"); expect(connectionManagerStub.connect).to.have.been.calledOnce; }); - test("returns error when auto-connect fails", async () => { // Setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -2018,24 +1632,19 @@ suite("DacFxApplicationWebviewController", () => { authenticationType: 2, // SqlLogin }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); connectionManagerStub.connect.resolves(false); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialProfileId: "profile1", }); - // Verify expect(result.autoConnected).to.be.false; expect(result.errorMessage).to.exist; }); - test("fetches ownerUri for already active connection", async () => { // Setup const activeOwnerUri = "active-owner-uri"; @@ -2048,8 +1657,12 @@ suite("DacFxApplicationWebviewController", () => { authenticationType: 1, // Integrated }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); + // Stub findMatchingProfile to return the matching connection + connStoreStub.findMatchingProfile.resolves({ + profile: recentConns[0], + score: 2, // ServerAndDatabase match + }); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({ [activeOwnerUri]: { credentials: recentConns[0], @@ -2058,35 +1671,27 @@ suite("DacFxApplicationWebviewController", () => { })); connectionManagerStub.getUriForConnection.returns(activeOwnerUri); connectionManagerStub.isConnected.withArgs(activeOwnerUri).returns(true); - createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialServerName: "server1", }); - // Verify expect(result.selectedConnection).to.exist; expect(result.ownerUri).to.equal(activeOwnerUri); - expect(result.autoConnected).to.be.false; // Already was connected + expect(result.autoConnected).to.be.true; // connectToServer was called and returned success }); - test("handles exception during initialization gracefully", async () => { // Setup - connStoreStub.getRecentlyUsedConnections.throws(new Error("Store error")); - + connStoreStub.readAllConnections.throws(new Error("Store error")); createController(); - // Execute const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); expect(handler, "InitializeConnection handler should be registered").to.exist; - const result = await handler!({ initialProfileId: "profile1", }); - // Verify - when store fails, listConnections catches it and returns empty array // initializeConnection then returns empty array with no match found expect(result.connections).to.exist; @@ -2095,7 +1700,6 @@ suite("DacFxApplicationWebviewController", () => { expect(result.selectedConnection).to.be.undefined; // No error message is set because listConnections handles the error internally }); - test("prioritizes profile ID match over server name match", async () => { // Setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -2113,33 +1717,27 @@ suite("DacFxApplicationWebviewController", () => { authenticationType: 1, // Integrated }, ]; - - connStoreStub.getRecentlyUsedConnections.returns(recentConns); + connStoreStub.readAllConnections.resolves(recentConns); sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - // Execute - both profile ID and server name match, profile ID should win const handler = requestHandlers.get(InitializeConnectionWebviewRequest.type.method); const result = await handler!({ initialProfileId: "profile2", initialServerName: "server1", }); - // Verify - should match profile2 by ID, not profile1 by server expect(result.selectedConnection).to.exist; expect(result.selectedConnection?.profileId).to.equal("profile2"); expect(result.selectedConnection?.database).to.equal("db2"); }); }); - suite("Telemetry", () => { let startActivityStub: sinon.SinonStub; let endStub: sinon.SinonStub; let endFailedStub: sinon.SinonStub; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockActivity: any; - setup(() => { endStub = sandbox.stub(); endFailedStub = sandbox.stub(); @@ -2152,7 +1750,6 @@ suite("DacFxApplicationWebviewController", () => { }; startActivityStub = sandbox.stub(telemetry, "startActivity").returns(mockActivity); }); - suite("Deploy DACPAC", () => { test("sends telemetry on successful deploy to new database", async () => { const mockResult: DacFxResult = { @@ -2160,10 +1757,8 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "operation-123", }; - dacFxServiceStub.deployDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\test\\database.dacpac", @@ -2171,9 +1766,7 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: true, ownerUri: ownerUri, }; - await requestHandler!(params); - // Verify startActivity was called with correct parameters (twice: once for Load, once for the operation) expect(startActivityStub).to.have.been.calledTwice; expect(startActivityStub.secondCall).to.have.been.calledWith( @@ -2182,25 +1775,20 @@ suite("DacFxApplicationWebviewController", () => { undefined, sinon.match({ isNewDatabase: "true" }), ); - // Verify end was called for success expect(endStub).to.have.been.calledOnce; expect(endStub).to.have.been.calledWith(ActivityStatus.Succeeded); - // Verify endFailed was not called expect(endFailedStub).to.not.have.been.called; }); - test("sends telemetry on deploy failure from service", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Deployment failed: Permission denied", operationId: "operation-789", }; - dacFxServiceStub.deployDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\test\\database.dacpac", @@ -2208,24 +1796,18 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: true, ownerUri: ownerUri, }; - await requestHandler!(params); - // Verify startActivity was called (twice: once for Load, once for the operation) expect(startActivityStub).to.have.been.calledTwice; - // Verify endFailed was called for service failure expect(endFailedStub).to.have.been.calledOnce; expect(endFailedStub).to.have.been.calledWith(sinon.match.instanceOf(Error), false); - // Verify end was not called expect(endStub).to.not.have.been.called; }); - test("sends telemetry on deploy exception", async () => { dacFxServiceStub.deployDacpac.rejects(new Error("Network timeout")); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\test\\database.dacpac", @@ -2233,26 +1815,20 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: true, ownerUri: ownerUri, }; - await requestHandler!(params); - // Verify endFailed was called for exception expect(endFailedStub).to.have.been.calledOnce; expect(endFailedStub).to.have.been.calledWith(sinon.match.instanceOf(Error), false); - expect(endStub).to.not.have.been.called; }); - test("does not include sensitive data in telemetry", async () => { const mockResult: DacFxResult = { success: true, errorMessage: undefined, operationId: "operation-123", }; - dacFxServiceStub.deployDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\private\\database.dacpac", @@ -2260,24 +1836,19 @@ suite("DacFxApplicationWebviewController", () => { isNewDatabase: true, ownerUri: ownerUri, }; - await requestHandler!(params); - // Verify that additional properties don't contain sensitive data const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry const additionalProps = startActivityCall.args[3]; - // Should not contain database name or file path expect(additionalProps).to.not.have.property("databaseName"); expect(additionalProps).to.not.have.property("packageFilePath"); - // Verify end call doesn't contain sensitive data expect(endStub).to.have.been.calledOnce; expect(endStub).to.have.been.calledWith(ActivityStatus.Succeeded); expect(endStub.firstCall.args).to.have.lengthOf(1); // Only status, no additional properties }); }); - suite("Extract DACPAC", () => { test("sends telemetry on successful extract", async () => { const mockResult: DacFxResult = { @@ -2285,10 +1856,8 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "extract-123", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); const params = { databaseName: "SourceDatabase", @@ -2297,31 +1866,25 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "1.0.0", ownerUri: ownerUri, }; - await requestHandler!(params); - // Verify telemetry was started expect(startActivityStub).to.have.been.calledTwice; // Load + Extract expect(startActivityStub.secondCall).to.have.been.calledWith( TelemetryViews.DacFxApplication, TelemetryActions.DacFxExtractDacpac, ); - // Verify end was called for success expect(endStub).to.have.been.calledOnce; expect(endFailedStub).to.not.have.been.called; }); - test("sends telemetry on extract failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Extraction failed: Database not found", operationId: "extract-456", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); const params = { databaseName: "NonExistentDatabase", @@ -2330,24 +1893,19 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "1.0.0", ownerUri: ownerUri, }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Extract expect(endFailedStub).to.have.been.calledOnce; expect(endStub).to.not.have.been.called; }); - test("does not include sensitive data in telemetry", async () => { const mockResult: DacFxResult = { success: true, errorMessage: undefined, operationId: "extract-123", }; - dacFxServiceStub.extractDacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); const params = { databaseName: "SensitiveDatabase", @@ -2356,18 +1914,14 @@ suite("DacFxApplicationWebviewController", () => { applicationVersion: "2.0.0", ownerUri: ownerUri, }; - await requestHandler!(params); - // Verify startActivity doesn't contain sensitive data const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action - // Verify end doesn't contain sensitive data expect(endStub.firstCall.args).to.have.lengthOf(1); }); }); - suite("Import BACPAC", () => { test("sends telemetry on successful import", async () => { const mockResult: DacFxResult = { @@ -2375,78 +1929,62 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "import-123", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\backup\\database.bacpac", databaseName: "RestoredDatabase", ownerUri: ownerUri, }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Import expect(startActivityStub.secondCall).to.have.been.calledWith( TelemetryViews.DacFxApplication, TelemetryActions.DacFxImportBacpac, ); - expect(endStub).to.have.been.calledOnce; expect(endFailedStub).to.not.have.been.called; }); - test("sends telemetry on import failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Import failed: Corrupted BACPAC file", operationId: "import-456", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\backup\\corrupted.bacpac", databaseName: "TestDatabase", ownerUri: ownerUri, }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Import expect(endFailedStub).to.have.been.calledOnce; expect(endStub).to.not.have.been.called; }); - test("does not include sensitive data in telemetry", async () => { const mockResult: DacFxResult = { success: true, errorMessage: undefined, operationId: "import-123", }; - dacFxServiceStub.importBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); const params = { packageFilePath: "C:\\private\\database.bacpac", databaseName: "PrivateDatabase", ownerUri: ownerUri, }; - await requestHandler!(params); - const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action expect(endStub.firstCall.args).to.have.lengthOf(1); }); }); - suite("Export BACPAC", () => { test("sends telemetry on successful export", async () => { const mockResult: DacFxResult = { @@ -2454,72 +1992,57 @@ suite("DacFxApplicationWebviewController", () => { errorMessage: undefined, operationId: "export-123", }; - dacFxServiceStub.exportBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); const params = { databaseName: "SourceDatabase", packageFilePath: "C:\\backup\\database.bacpac", ownerUri: ownerUri, }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Export expect(startActivityStub.secondCall).to.have.been.calledWith( TelemetryViews.DacFxApplication, TelemetryActions.DacFxExportBacpac, ); - expect(endStub).to.have.been.calledOnce; expect(endFailedStub).to.not.have.been.called; }); - test("sends telemetry on export failure", async () => { const mockResult: DacFxResult = { success: false, errorMessage: "Export failed: Insufficient permissions", operationId: "export-456", }; - dacFxServiceStub.exportBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); const params = { databaseName: "ProtectedDatabase", packageFilePath: "C:\\backup\\database.bacpac", ownerUri: ownerUri, }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Export expect(endFailedStub).to.have.been.calledOnce; expect(endStub).to.not.have.been.called; }); - test("does not include sensitive data in telemetry", async () => { const mockResult: DacFxResult = { success: true, errorMessage: undefined, operationId: "export-123", }; - dacFxServiceStub.exportBacpac.resolves(mockResult); createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); const params = { databaseName: "ConfidentialDatabase", packageFilePath: "C:\\secure\\database.bacpac", ownerUri: ownerUri, }; - await requestHandler!(params); - const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action expect(endStub.firstCall.args).to.have.lengthOf(1); From b0826be86d75ef05ac877e9125f11a7adf0d55d2 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 10:50:32 -0600 Subject: [PATCH 49/79] removing tests not needed after refactoring --- .../dacFxApplicationWebviewController.test.ts | 147 ------------------ 1 file changed, 147 deletions(-) diff --git a/test/unit/dacFxApplicationWebviewController.test.ts b/test/unit/dacFxApplicationWebviewController.test.ts index fb497e416b..08c421957d 100644 --- a/test/unit/dacFxApplicationWebviewController.test.ts +++ b/test/unit/dacFxApplicationWebviewController.test.ts @@ -1166,37 +1166,6 @@ suite("DacFxApplicationWebviewController", () => { expect(result).to.exist; expect(result.connections).to.be.an("array").that.is.empty; }); - test("builds display name correctly with all fields", async () => { - connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - const conn = result.connections[0]; - // getConnectionDisplayName returns profileName if available - expect(conn.displayName).to.equal("Server 1 - db1"); - }); - test("builds display name without optional fields", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const minimalConnection: any = { - server: "testserver", - database: undefined, - user: undefined, - profileName: undefined, - id: "conn-minimal", - authenticationType: 1, - }; - connectionStoreStub.readAllConnections.resolves([minimalConnection]); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - const conn = result.connections[0]; - // When profileName is not set, getConnectionDisplayName generates: server, database (authType) - // With authenticationType=1 (Integrated), database undefined -> "testserver, (1)" - expect(conn.displayName).to.include("testserver"); - expect(conn.displayName).to.include(""); - }); test("connects to server successfully when not already connected", async () => { connectionStoreStub.readAllConnections.resolves([mockConnections[0]]); connectionManagerStub.getUriForConnection.returns("new-owner-uri"); @@ -1298,65 +1267,6 @@ suite("DacFxApplicationWebviewController", () => { expect(result.errorMessage).to.include("Connection failed"); expect(result.errorMessage).to.include("Network timeout"); }); - test("identifies connected server by matching server and database", async () => { - connectionStoreStub.readAllConnections.resolves(mockConnections); - // Mock active connection with matching server and database - const mockActiveConnections = { - uri1: { - credentials: { - server: "localhost", - database: "master", - }, - }, - }; - sandbox - .stub(connectionManagerStub, "activeConnections") - .get(() => mockActiveConnections); - // Stub getUriForConnection and isConnected - connectionManagerStub.getUriForConnection.callsFake((profile) => { - if (profile.server === "localhost" && profile.database === "master") { - return "uri1"; - } - return undefined; - }); - connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - // Find localhost connection - const localhostConn = result.connections.find((c) => c.server === "localhost"); - expect(localhostConn).to.exist; - }); - test("identifies connected server when database is undefined in both", async () => { - const connectionWithoutDb = { - ...mockConnections[2], - database: undefined, - }; - connectionStoreStub.readAllConnections.resolves([connectionWithoutDb]); - // Mock active connection without database - const mockActiveConnections = { - uri1: { - credentials: { - server: "server2.database.windows.net", - database: undefined, - }, - }, - }; - sandbox - .stub(connectionManagerStub, "activeConnections") - .get(() => mockActiveConnections); - // Stub getUriForConnection and isConnected - connectionManagerStub.getUriForConnection.callsFake((profile) => { - if (profile.server === "server2.database.windows.net") { - return "uri1"; - } - return undefined; - }); - connectionManagerStub.isConnected.callsFake((uri) => uri === "uri1"); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - }); test("generates profileId from server and database when id is missing", async () => { const connectionWithoutId: (typeof mockConnections)[0] = { ...mockConnections[0], @@ -1369,63 +1279,6 @@ suite("DacFxApplicationWebviewController", () => { const result = await handler!({}); expect(result.connections[0].profileId).to.equal("server1.database.windows.net_db1"); }); - test("matches connection by server and database when both provided", async () => { - connectionStoreStub.readAllConnections.resolves(mockConnections); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - // Find the connection that matches server1.database.windows.net and db1 - const matchingConnection = result.connections.find( - (conn) => conn.server === "server1.database.windows.net" && conn.database === "db1", - ); - expect(matchingConnection).to.exist; - expect(matchingConnection!.profileId).to.equal("conn1"); - }); - test("matches connection by server only when database is not specified", async () => { - connectionStoreStub.readAllConnections.resolves(mockConnections); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - // Find the connection that matches localhost (conn2 has master database) - const matchingConnection = result.connections.find( - (conn) => conn.server === "localhost" && conn.database === "master", - ); - expect(matchingConnection).to.exist; - expect(matchingConnection!.profileId).to.equal("conn2"); - }); - test("finds connection when database is undefined in profile", async () => { - // This tests the scenario where a server-level connection exists - // (database is undefined in the connection profile) - connectionStoreStub.readAllConnections.resolves(mockConnections); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - // conn3 has undefined database - should still be findable by server - const matchingConnection = result.connections.find( - (conn) => conn.server === "server2.database.windows.net", - ); - expect(matchingConnection).to.exist; - expect(matchingConnection!.profileId).to.equal("conn3"); - expect(matchingConnection!.database).to.be.undefined; - }); - test("connection matching is case-sensitive for server names", async () => { - connectionStoreStub.readAllConnections.resolves(mockConnections); - sandbox.stub(connectionManagerStub, "activeConnections").get(() => ({})); - createController(); - const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); - const result = await handler!({}); - // Case must match exactly - const matchingConnection = result.connections.find( - (conn) => conn.server === "LOCALHOST", // Different case - ); - expect(matchingConnection).to.be.undefined; - // Correct case should work - const correctMatch = result.connections.find((conn) => conn.server === "localhost"); - expect(correctMatch).to.exist; - }); }); suite("Database Operations with Empty OwnerUri", () => { test("returns empty array when ownerUri is empty for list databases", async () => { From 317c47068f3b05038e621f8e40a70207dbecb734 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 10:59:06 -0600 Subject: [PATCH 50/79] refactor method --- .../dacFxApplicationWebviewController.ts | 225 +++++++++++------- 1 file changed, 144 insertions(+), 81 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 4dd6ab27d6..2b7b831437 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -599,45 +599,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get all connections const { connections } = await this.listConnections(); - let matchingConnection: dacFxApplication.ConnectionProfile | undefined; - - // Priority 1: Match by profile ID if provided - if (params.initialProfileId) { - matchingConnection = connections.find( - (conn) => conn.profileId === params.initialProfileId, - ); - if (matchingConnection) { - this.logger.verbose( - `Found connection by profile ID: ${params.initialProfileId}`, - ); - } - } - - // Priority 2: Use findMatchingProfile if we have server name - if (!matchingConnection && params.initialServerName) { - // Create a temporary profile to search with - const searchProfile = { - server: params.initialServerName, - database: params.initialDatabaseName || "", - } as IConnectionProfile; - - const matchResult = - await this.connectionManager.connectionStore.findMatchingProfile(searchProfile); - - if (matchResult?.profile) { - // Find the matching connection in our list - const profileId = - matchResult.profile.id || - `${matchResult.profile.server}_${matchResult.profile.database || ""}`; - matchingConnection = connections.find((conn) => conn.profileId === profileId); - - if (matchingConnection) { - this.logger.verbose( - `Found connection by server/database using findMatchingProfile: ${params.initialServerName}/${params.initialDatabaseName || "default"}`, - ); - } - } - } + // Find matching connection based on initial parameters + const matchingConnection = await this.findMatchingConnection(params, connections); if (!matchingConnection) { // No match found - return all connections, let user choose @@ -648,64 +611,164 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll }; } - // Found a matching connection - - // Case 1: Already connected via Object Explorer (ownerUri provided) + // Handle existing connection from Object Explorer if (params.initialOwnerUri) { + return this.useExistingConnection( + connections, + matchingConnection, + params.initialOwnerUri, + ); + } + + // Attempt to connect to the matched profile + return await this.connectToMatchedProfile(connections, matchingConnection); + } catch (error) { + this.logger.error(`Failed to initialize connection: ${error}`); + // Fallback: return empty state + return { + connections: [], + autoConnected: false, + errorMessage: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Finds a matching connection profile based on profile ID or server/database name + */ + private async findMatchingConnection( + params: { + initialProfileId?: string; + initialServerName?: string; + initialDatabaseName?: string; + }, + connections: dacFxApplication.ConnectionProfile[], + ): Promise { + // Priority 1: Match by profile ID if provided + if (params.initialProfileId) { + const matchingConnection = connections.find( + (conn) => conn.profileId === params.initialProfileId, + ); + if (matchingConnection) { + this.logger.verbose(`Found connection by profile ID: ${params.initialProfileId}`); + return matchingConnection; + } + } + + // Priority 2: Use findMatchingProfile if we have server name + if (params.initialServerName) { + return await this.findConnectionByServerName( + params.initialServerName, + params.initialDatabaseName, + connections, + ); + } + + return undefined; + } + + /** + * Finds a connection by server and database name using the connection store's matching logic + */ + private async findConnectionByServerName( + serverName: string, + databaseName: string | undefined, + connections: dacFxApplication.ConnectionProfile[], + ): Promise { + // Create a temporary profile to search with + const searchProfile = { + server: serverName, + database: databaseName || "", + } as IConnectionProfile; + + const matchResult = + await this.connectionManager.connectionStore.findMatchingProfile(searchProfile); + + if (matchResult?.profile) { + // Find the matching connection in our list + const profileId = + matchResult.profile.id || + `${matchResult.profile.server}_${matchResult.profile.database || ""}`; + const matchingConnection = connections.find((conn) => conn.profileId === profileId); + + if (matchingConnection) { this.logger.verbose( - `Using existing connection from Object Explorer: ${params.initialOwnerUri}`, + `Found connection by server/database using findMatchingProfile: ${serverName}/${databaseName || "default"}`, ); + return matchingConnection; + } + } + + return undefined; + } + + /** + * Returns result for an existing connection (from Object Explorer) + */ + private useExistingConnection( + connections: dacFxApplication.ConnectionProfile[], + matchingConnection: dacFxApplication.ConnectionProfile, + ownerUri: string, + ): { + connections: dacFxApplication.ConnectionProfile[]; + selectedConnection: dacFxApplication.ConnectionProfile; + ownerUri: string; + autoConnected: boolean; + } { + this.logger.verbose(`Using existing connection from Object Explorer: ${ownerUri}`); + return { + connections, + selectedConnection: matchingConnection, + ownerUri, + autoConnected: false, // Was already connected + }; + } + + /** + * Attempts to connect to a matched profile and returns the result + */ + private async connectToMatchedProfile( + connections: dacFxApplication.ConnectionProfile[], + matchingConnection: dacFxApplication.ConnectionProfile, + ): Promise<{ + connections: dacFxApplication.ConnectionProfile[]; + selectedConnection: dacFxApplication.ConnectionProfile; + ownerUri?: string; + autoConnected: boolean; + errorMessage?: string; + }> { + this.logger.verbose(`Connecting to profile: ${matchingConnection.profileId}`); + try { + const connectResult = await this.connectToServer(matchingConnection.profileId); + + if (connectResult.isConnected && connectResult.ownerUri) { + this.logger.info(`Connected to: ${matchingConnection.server}`); return { connections, selectedConnection: matchingConnection, - ownerUri: params.initialOwnerUri, - autoConnected: false, // Was already connected + ownerUri: connectResult.ownerUri, + autoConnected: true, }; - } - - // Case 2: Connect to the matched profile - // connectToServer handles checking if already connected internally - this.logger.verbose(`Connecting to profile: ${matchingConnection.profileId}`); - try { - const connectResult = await this.connectToServer(matchingConnection.profileId); - - if (connectResult.isConnected && connectResult.ownerUri) { - this.logger.info(`Connected to: ${matchingConnection.server}`); - return { - connections, - selectedConnection: matchingConnection, - ownerUri: connectResult.ownerUri, - autoConnected: true, - }; - } else { - // Connection failed - this.logger.error( - `Connection failed: ${connectResult.errorMessage || "Unknown error"}`, - ); - return { - connections, - selectedConnection: matchingConnection, - autoConnected: false, - errorMessage: connectResult.errorMessage, - }; - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - this.logger.error(`Connection exception: ${errorMsg}`); + } else { + // Connection failed + this.logger.error( + `Connection failed: ${connectResult.errorMessage || "Unknown error"}`, + ); return { connections, selectedConnection: matchingConnection, autoConnected: false, - errorMessage: errorMsg, + errorMessage: connectResult.errorMessage, }; } } catch (error) { - this.logger.error(`Failed to initialize connection: ${error}`); - // Fallback: return empty state + const errorMsg = error instanceof Error ? error.message : String(error); + this.logger.error(`Connection exception: ${errorMsg}`); return { - connections: [], + connections, + selectedConnection: matchingConnection, autoConnected: false, - errorMessage: error instanceof Error ? error.message : String(error), + errorMessage: errorMsg, }; } } From 2cc5d33d0c7f5bb9f19626006e403a0503b89503 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 11:00:37 -0600 Subject: [PATCH 51/79] moving styles to the bottom --- .../DacFxApplication/dacFxApplicationForm.tsx | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index c86d58b5a6..5433a31272 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -30,42 +30,6 @@ interface ValidationMessage { */ const DEFAULT_APPLICATION_VERSION = "1.0.0"; -const useStyles = makeStyles({ - root: { - display: "flex", - flexDirection: "column", - width: "100%", - maxHeight: "100vh", - overflowY: "auto", - padding: "10px", - }, - formContainer: { - display: "flex", - flexDirection: "column", - width: "700px", - maxWidth: "calc(100% - 20px)", - gap: "16px", - }, - title: { - fontSize: tokens.fontSizeBase500, - fontWeight: tokens.fontWeightSemibold, - marginBottom: "8px", - }, - description: { - fontSize: tokens.fontSizeBase300, - color: tokens.colorNeutralForeground2, - marginBottom: "16px", - }, - actions: { - display: "flex", - gap: "8px", - justifyContent: "flex-end", - marginTop: "16px", - paddingTop: "16px", - borderTop: `1px solid ${tokens.colorNeutralStroke2}`, - }, -}); - export const DacFxApplicationForm = () => { const classes = useStyles(); const context = useContext(DacFxApplicationContext); @@ -754,3 +718,39 @@ export const DacFxApplicationForm = () => {
); }; + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + width: "100%", + maxHeight: "100vh", + overflowY: "auto", + padding: "10px", + }, + formContainer: { + display: "flex", + flexDirection: "column", + width: "700px", + maxWidth: "calc(100% - 20px)", + gap: "16px", + }, + title: { + fontSize: tokens.fontSizeBase500, + fontWeight: tokens.fontWeightSemibold, + marginBottom: "8px", + }, + description: { + fontSize: tokens.fontSizeBase300, + color: tokens.colorNeutralForeground2, + marginBottom: "16px", + }, + actions: { + display: "flex", + gap: "8px", + justifyContent: "flex-end", + marginTop: "16px", + paddingTop: "16px", + borderTop: `1px solid ${tokens.colorNeutralStroke2}`, + }, +}); From 15fa192956c6e2f3c6c583a68ce037562e4abefc Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 12:14:39 -0600 Subject: [PATCH 52/79] changes to use common interface --- .../dacFxApplicationWebviewController.ts | 91 ++++++++----------- .../ServerSelectionSection.tsx | 22 +++-- .../DacFxApplication/dacFxApplicationForm.tsx | 13 ++- src/sharedInterfaces/dacFxApplication.ts | 39 +------- .../dacFxApplicationWebviewController.test.ts | 20 ++-- 5 files changed, 71 insertions(+), 114 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 2b7b831437..297feb2ef9 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -20,6 +20,7 @@ import { TelemetryViews, TelemetryActions, ActivityStatus } from "../sharedInter import * as dacFxApplication from "../sharedInterfaces/dacFxApplication"; import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; +import { IConnectionDialogProfile } from "../sharedInterfaces/connectionDialog"; import { getConnectionDisplayName } from "../models/connectionInfo"; // File extension constants @@ -545,32 +546,32 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Lists all available connections from the connection store */ private async listConnections(): Promise<{ - connections: dacFxApplication.ConnectionProfile[]; + connections: IConnectionDialogProfile[]; }> { try { - const connections: dacFxApplication.ConnectionProfile[] = []; - // Get all saved connections from connection store (saved profiles only, not recent connections) const savedConnections = await this.connectionManager.connectionStore.readAllConnections(); - // Build the connection profile list from saved connections - for (const conn of savedConnections) { + // Convert to IConnectionDialogProfile format and ensure profileName is set + const connections: IConnectionDialogProfile[] = savedConnections.map((conn) => { const profile = conn as IConnectionProfile; - const displayName = getConnectionDisplayName(profile); - const profileId = profile.id || `${profile.server}_${profile.database || ""}`; + // Use getConnectionDisplayName if profileName is not set + const displayName = profile.profileName || getConnectionDisplayName(profile); - connections.push({ - displayName, + return { server: profile.server, database: profile.database, - authenticationType: this.getAuthenticationTypeString( - profile.authenticationType, - ), - userName: profile.user, - profileId, - }); - } + user: profile.user, + password: profile.password, + authenticationType: profile.authenticationType, + profileName: displayName, + id: profile.id || `${profile.server}_${profile.database || ""}`, + groupId: profile.groupId, + savePassword: profile.savePassword, + azureAuthType: profile.azureAuthType, + } as IConnectionDialogProfile; + }); return { connections }; } catch (error) { @@ -589,8 +590,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll initialOwnerUri?: string; initialProfileId?: string; }): Promise<{ - connections: dacFxApplication.ConnectionProfile[]; - selectedConnection?: dacFxApplication.ConnectionProfile; + connections: IConnectionDialogProfile[]; + selectedConnection?: IConnectionDialogProfile; ownerUri?: string; autoConnected: boolean; errorMessage?: string; @@ -642,12 +643,12 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll initialServerName?: string; initialDatabaseName?: string; }, - connections: dacFxApplication.ConnectionProfile[], - ): Promise { + connections: IConnectionDialogProfile[], + ): Promise { // Priority 1: Match by profile ID if provided if (params.initialProfileId) { const matchingConnection = connections.find( - (conn) => conn.profileId === params.initialProfileId, + (conn) => conn.id === params.initialProfileId, ); if (matchingConnection) { this.logger.verbose(`Found connection by profile ID: ${params.initialProfileId}`); @@ -673,8 +674,8 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll private async findConnectionByServerName( serverName: string, databaseName: string | undefined, - connections: dacFxApplication.ConnectionProfile[], - ): Promise { + connections: IConnectionDialogProfile[], + ): Promise { // Create a temporary profile to search with const searchProfile = { server: serverName, @@ -689,7 +690,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll const profileId = matchResult.profile.id || `${matchResult.profile.server}_${matchResult.profile.database || ""}`; - const matchingConnection = connections.find((conn) => conn.profileId === profileId); + const matchingConnection = connections.find((conn) => { + const connId = conn.id || `${conn.server}_${conn.database || ""}`; + return connId === profileId; + }); if (matchingConnection) { this.logger.verbose( @@ -706,12 +710,12 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Returns result for an existing connection (from Object Explorer) */ private useExistingConnection( - connections: dacFxApplication.ConnectionProfile[], - matchingConnection: dacFxApplication.ConnectionProfile, + connections: IConnectionDialogProfile[], + matchingConnection: IConnectionDialogProfile, ownerUri: string, ): { - connections: dacFxApplication.ConnectionProfile[]; - selectedConnection: dacFxApplication.ConnectionProfile; + connections: IConnectionDialogProfile[]; + selectedConnection: IConnectionDialogProfile; ownerUri: string; autoConnected: boolean; } { @@ -728,18 +732,18 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Attempts to connect to a matched profile and returns the result */ private async connectToMatchedProfile( - connections: dacFxApplication.ConnectionProfile[], - matchingConnection: dacFxApplication.ConnectionProfile, + connections: IConnectionDialogProfile[], + matchingConnection: IConnectionDialogProfile, ): Promise<{ - connections: dacFxApplication.ConnectionProfile[]; - selectedConnection: dacFxApplication.ConnectionProfile; + connections: IConnectionDialogProfile[]; + selectedConnection: IConnectionDialogProfile; ownerUri?: string; autoConnected: boolean; errorMessage?: string; }> { - this.logger.verbose(`Connecting to profile: ${matchingConnection.profileId}`); + this.logger.verbose(`Connecting to profile: ${matchingConnection.id}`); try { - const connectResult = await this.connectToServer(matchingConnection.profileId); + const connectResult = await this.connectToServer(matchingConnection.id!); if (connectResult.isConnected && connectResult.ownerUri) { this.logger.info(`Connected to: ${matchingConnection.server}`); @@ -844,25 +848,6 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll } } - /** - * Builds a display name for a connection profile - */ - /** - * Gets a string representation of the authentication type - */ - private getAuthenticationTypeString(authType: number | string | undefined): string { - switch (authType) { - case 1: - return "Integrated"; - case 2: - return "SQL Login"; - case 3: - return "Azure MFA"; - default: - return "Unknown"; - } - } - /** * Formats the current date/time as yyyy-MM-dd-HH-mm for use in filenames */ diff --git a/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx b/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx index 656318eb33..93621bd622 100644 --- a/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx +++ b/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Dropdown, Field, makeStyles, Option, Spinner } from "@fluentui/react-components"; -import { ConnectionProfile } from "../../../sharedInterfaces/dacFxApplication"; +import { IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog"; import { locConstants } from "../../common/locConstants"; /** @@ -17,7 +17,7 @@ interface ValidationMessage { interface ServerSelectionSectionProps { selectedProfileId: string; - availableConnections: ConnectionProfile[]; + availableConnections: IConnectionDialogProfile[]; isConnecting: boolean; isOperationInProgress: boolean; validationMessages: Record; @@ -58,9 +58,12 @@ export const ServerSelectionSection = ({ placeholder={locConstants.dacFxApplication.selectServer} value={ selectedProfileId - ? availableConnections.find( - (conn) => conn.profileId === selectedProfileId, - )?.displayName || "" + ? (() => { + const conn = availableConnections.find( + (conn) => conn.id === selectedProfileId, + ); + return conn?.profileName || ""; + })() : "" } selectedOptions={selectedProfileId ? [selectedProfileId] : []} @@ -76,11 +79,10 @@ export const ServerSelectionSection = ({ ) : ( availableConnections.map((conn) => ( )) )} diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index 5433a31272..8ed58ab8c4 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -7,6 +7,7 @@ import { Button, makeStyles, tokens } from "@fluentui/react-components"; import { DatabaseArrowRight20Regular } from "@fluentui/react-icons"; import { useState, useEffect, useContext } from "react"; import * as dacFxApplication from "../../../sharedInterfaces/dacFxApplication"; +import { IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog"; import { locConstants } from "../../common/locConstants"; import { ApplicationInfoSection } from "./ApplicationInfoSection"; import { DacFxApplicationContext } from "./dacFxApplicationStateProvider"; @@ -59,9 +60,9 @@ export const DacFxApplicationForm = () => { const [validationMessages, setValidationMessages] = useState>( {}, ); - const [availableConnections, setAvailableConnections] = useState< - dacFxApplication.ConnectionProfile[] - >([]); + const [availableConnections, setAvailableConnections] = useState( + [], + ); const [selectedProfileId, setSelectedProfileId] = useState( initialSelectedProfileId || "", ); @@ -141,7 +142,7 @@ export const DacFxApplicationForm = () => { // If a connection was selected/matched if (result.selectedConnection) { - setSelectedProfileId(result.selectedConnection.profileId); + setSelectedProfileId(result.selectedConnection.id!); // If we have an ownerUri (either provided or from auto-connect) if (result.ownerUri) { @@ -178,9 +179,7 @@ export const DacFxApplicationForm = () => { setValidationMessages({}); // Find the selected connection - const selectedConnection = availableConnections.find( - (conn) => conn.profileId === profileId, - ); + const selectedConnection = availableConnections.find((conn) => conn.id === profileId); if (!selectedConnection) { return; diff --git a/src/sharedInterfaces/dacFxApplication.ts b/src/sharedInterfaces/dacFxApplication.ts index 53422bc98e..53a5ae04ad 100644 --- a/src/sharedInterfaces/dacFxApplication.ts +++ b/src/sharedInterfaces/dacFxApplication.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { NotificationType, RequestType } from "vscode-jsonrpc/browser"; +import { IConnectionDialogProfile } from "./connectionDialog"; /** * The type of DacFx Application operation to perform @@ -15,36 +16,6 @@ export enum DacFxOperationType { Export = "export", } -/** - * Simplified connection profile for display in UI - */ -export interface ConnectionProfile { - /** - * Display name for the connection - */ - displayName: string; - /** - * Server name - */ - server: string; - /** - * Database name (if specified) - */ - database?: string; - /** - * Authentication type - */ - authenticationType: string; - /** - * User name (for SQL Auth) - */ - userName?: string; - /** - * The profile ID used to identify this connection - */ - profileId: string; -} - /** * The state of the DacFx Application webview */ @@ -76,7 +47,7 @@ export interface DacFxApplicationWebviewState { /** * List of available connection profiles */ - availableConnections?: ConnectionProfile[]; + availableConnections?: IConnectionDialogProfile[]; /** * Whether to create a new database or upgrade existing (for Deploy) */ @@ -231,7 +202,7 @@ export namespace ValidateDatabaseNameWebviewRequest { * Request to list available connections from the webview */ export namespace ListConnectionsWebviewRequest { - export const type = new RequestType( + export const type = new RequestType( "dacFxApplication/listConnections", ); } @@ -249,8 +220,8 @@ export namespace InitializeConnectionWebviewRequest { initialProfileId?: string; }, { - connections: ConnectionProfile[]; - selectedConnection?: ConnectionProfile; + connections: IConnectionDialogProfile[]; + selectedConnection?: IConnectionDialogProfile; ownerUri?: string; autoConnected: boolean; errorMessage?: string; diff --git a/test/unit/dacFxApplicationWebviewController.test.ts b/test/unit/dacFxApplicationWebviewController.test.ts index 08c421957d..156e8b5db7 100644 --- a/test/unit/dacFxApplicationWebviewController.test.ts +++ b/test/unit/dacFxApplicationWebviewController.test.ts @@ -1089,7 +1089,7 @@ suite("DacFxApplicationWebviewController", () => { user: "admin", profileName: "Server 1 - db1", id: "conn1", - authenticationType: 2, // SQL Login + authenticationType: "SqlLogin", // SQL Login }, { server: "localhost", @@ -1097,7 +1097,7 @@ suite("DacFxApplicationWebviewController", () => { user: undefined, profileName: "Local Server", id: "conn2", - authenticationType: 1, // Integrated + authenticationType: "Integrated", // Integrated }, { server: "server2.database.windows.net", @@ -1105,7 +1105,7 @@ suite("DacFxApplicationWebviewController", () => { user: "user@domain.com", profileName: "Azure Server", id: "conn3", - authenticationType: 3, // Azure MFA + authenticationType: "AzureMFA", // Azure MFA }, ]; }); @@ -1144,10 +1144,10 @@ suite("DacFxApplicationWebviewController", () => { const conn1 = result.connections[0]; expect(conn1.server).to.equal("server1.database.windows.net"); expect(conn1.database).to.equal("db1"); - expect(conn1.userName).to.equal("admin"); - expect(conn1.authenticationType).to.equal("SQL Login"); - expect(conn1.profileId).to.equal("conn1"); - expect(conn1.displayName).to.include("Server 1 - db1"); + expect(conn1.user).to.equal("admin"); + expect(conn1.authenticationType).to.equal("SqlLogin"); + expect(conn1.id).to.equal("conn1"); + expect(conn1.profileName).to.include("Server 1 - db1"); // Verify second connection const conn2 = result.connections[1]; expect(conn2.server).to.equal("localhost"); @@ -1155,7 +1155,7 @@ suite("DacFxApplicationWebviewController", () => { // Verify third connection const conn3 = result.connections[2]; expect(conn3.server).to.equal("server2.database.windows.net"); - expect(conn3.authenticationType).to.equal("Azure MFA"); + expect(conn3.authenticationType).to.equal("AzureMFA"); }); test("returns empty array when readAllConnections fails", async () => { connectionStoreStub.readAllConnections.rejects(new Error("Connection store error")); @@ -1277,7 +1277,7 @@ suite("DacFxApplicationWebviewController", () => { createController(); const handler = requestHandlers.get(ListConnectionsWebviewRequest.type.method); const result = await handler!({}); - expect(result.connections[0].profileId).to.equal("server1.database.windows.net_db1"); + expect(result.connections[0].id).to.equal("server1.database.windows.net_db1"); }); }); suite("Database Operations with Empty OwnerUri", () => { @@ -1581,7 +1581,7 @@ suite("DacFxApplicationWebviewController", () => { }); // Verify - should match profile2 by ID, not profile1 by server expect(result.selectedConnection).to.exist; - expect(result.selectedConnection?.profileId).to.equal("profile2"); + expect(result.selectedConnection?.id).to.equal("profile2"); expect(result.selectedConnection?.database).to.equal("db2"); }); }); From badecf17b30337c833ff34c898393d0cb3ee0e28 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 12:40:11 -0600 Subject: [PATCH 53/79] removing unit tests per PR request --- .../dacFxApplicationWebviewController.test.ts | 323 ------------------ 1 file changed, 323 deletions(-) diff --git a/test/unit/dacFxApplicationWebviewController.test.ts b/test/unit/dacFxApplicationWebviewController.test.ts index 156e8b5db7..c2c05b97ca 100644 --- a/test/unit/dacFxApplicationWebviewController.test.ts +++ b/test/unit/dacFxApplicationWebviewController.test.ts @@ -56,12 +56,6 @@ import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; import { ConnectionStore } from "../../src/models/connectionStore"; import * as fs from "fs"; import * as path from "path"; -import * as telemetry from "../../src/telemetry/telemetry"; -import { - TelemetryViews, - TelemetryActions, - ActivityStatus, -} from "../../src/sharedInterfaces/telemetry"; chai.use(sinonChai); suite("DacFxApplicationWebviewController", () => { let sandbox: sinon.SinonSandbox; @@ -1585,321 +1579,4 @@ suite("DacFxApplicationWebviewController", () => { expect(result.selectedConnection?.database).to.equal("db2"); }); }); - suite("Telemetry", () => { - let startActivityStub: sinon.SinonStub; - let endStub: sinon.SinonStub; - let endFailedStub: sinon.SinonStub; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let mockActivity: any; - setup(() => { - endStub = sandbox.stub(); - endFailedStub = sandbox.stub(); - mockActivity = { - end: endStub, - endFailed: endFailedStub, - correlationId: "test-correlation-id", - startTime: 0, - update: sandbox.stub(), - }; - startActivityStub = sandbox.stub(telemetry, "startActivity").returns(mockActivity); - }); - suite("Deploy DACPAC", () => { - test("sends telemetry on successful deploy to new database", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "operation-123", - }; - dacFxServiceStub.deployDacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\test\\database.dacpac", - databaseName: "NewDatabase", - isNewDatabase: true, - ownerUri: ownerUri, - }; - await requestHandler!(params); - // Verify startActivity was called with correct parameters (twice: once for Load, once for the operation) - expect(startActivityStub).to.have.been.calledTwice; - expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DacFxApplication, - TelemetryActions.DacFxDeployDacpac, - undefined, - sinon.match({ isNewDatabase: "true" }), - ); - // Verify end was called for success - expect(endStub).to.have.been.calledOnce; - expect(endStub).to.have.been.calledWith(ActivityStatus.Succeeded); - // Verify endFailed was not called - expect(endFailedStub).to.not.have.been.called; - }); - test("sends telemetry on deploy failure from service", async () => { - const mockResult: DacFxResult = { - success: false, - errorMessage: "Deployment failed: Permission denied", - operationId: "operation-789", - }; - dacFxServiceStub.deployDacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\test\\database.dacpac", - databaseName: "TestDatabase", - isNewDatabase: true, - ownerUri: ownerUri, - }; - await requestHandler!(params); - // Verify startActivity was called (twice: once for Load, once for the operation) - expect(startActivityStub).to.have.been.calledTwice; - // Verify endFailed was called for service failure - expect(endFailedStub).to.have.been.calledOnce; - expect(endFailedStub).to.have.been.calledWith(sinon.match.instanceOf(Error), false); - // Verify end was not called - expect(endStub).to.not.have.been.called; - }); - test("sends telemetry on deploy exception", async () => { - dacFxServiceStub.deployDacpac.rejects(new Error("Network timeout")); - createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\test\\database.dacpac", - databaseName: "TestDatabase", - isNewDatabase: true, - ownerUri: ownerUri, - }; - await requestHandler!(params); - // Verify endFailed was called for exception - expect(endFailedStub).to.have.been.calledOnce; - expect(endFailedStub).to.have.been.calledWith(sinon.match.instanceOf(Error), false); - expect(endStub).to.not.have.been.called; - }); - test("does not include sensitive data in telemetry", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "operation-123", - }; - dacFxServiceStub.deployDacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(DeployDacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\private\\database.dacpac", - databaseName: "SensitiveDatabase", - isNewDatabase: true, - ownerUri: ownerUri, - }; - await requestHandler!(params); - // Verify that additional properties don't contain sensitive data - const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry - const additionalProps = startActivityCall.args[3]; - // Should not contain database name or file path - expect(additionalProps).to.not.have.property("databaseName"); - expect(additionalProps).to.not.have.property("packageFilePath"); - // Verify end call doesn't contain sensitive data - expect(endStub).to.have.been.calledOnce; - expect(endStub).to.have.been.calledWith(ActivityStatus.Succeeded); - expect(endStub.firstCall.args).to.have.lengthOf(1); // Only status, no additional properties - }); - }); - suite("Extract DACPAC", () => { - test("sends telemetry on successful extract", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "extract-123", - }; - dacFxServiceStub.extractDacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); - const params = { - databaseName: "SourceDatabase", - packageFilePath: "C:\\output\\database.dacpac", - applicationName: "MyApp", - applicationVersion: "1.0.0", - ownerUri: ownerUri, - }; - await requestHandler!(params); - // Verify telemetry was started - expect(startActivityStub).to.have.been.calledTwice; // Load + Extract - expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DacFxApplication, - TelemetryActions.DacFxExtractDacpac, - ); - // Verify end was called for success - expect(endStub).to.have.been.calledOnce; - expect(endFailedStub).to.not.have.been.called; - }); - test("sends telemetry on extract failure", async () => { - const mockResult: DacFxResult = { - success: false, - errorMessage: "Extraction failed: Database not found", - operationId: "extract-456", - }; - dacFxServiceStub.extractDacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); - const params = { - databaseName: "NonExistentDatabase", - packageFilePath: "C:\\output\\database.dacpac", - applicationName: "MyApp", - applicationVersion: "1.0.0", - ownerUri: ownerUri, - }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Extract - expect(endFailedStub).to.have.been.calledOnce; - expect(endStub).to.not.have.been.called; - }); - test("does not include sensitive data in telemetry", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "extract-123", - }; - dacFxServiceStub.extractDacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ExtractDacpacWebviewRequest.type.method); - const params = { - databaseName: "SensitiveDatabase", - packageFilePath: "C:\\private\\database.dacpac", - applicationName: "PrivateApp", - applicationVersion: "2.0.0", - ownerUri: ownerUri, - }; - await requestHandler!(params); - // Verify startActivity doesn't contain sensitive data - const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry - expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action - // Verify end doesn't contain sensitive data - expect(endStub.firstCall.args).to.have.lengthOf(1); - }); - }); - suite("Import BACPAC", () => { - test("sends telemetry on successful import", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "import-123", - }; - dacFxServiceStub.importBacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\backup\\database.bacpac", - databaseName: "RestoredDatabase", - ownerUri: ownerUri, - }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Import - expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DacFxApplication, - TelemetryActions.DacFxImportBacpac, - ); - expect(endStub).to.have.been.calledOnce; - expect(endFailedStub).to.not.have.been.called; - }); - test("sends telemetry on import failure", async () => { - const mockResult: DacFxResult = { - success: false, - errorMessage: "Import failed: Corrupted BACPAC file", - operationId: "import-456", - }; - dacFxServiceStub.importBacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\backup\\corrupted.bacpac", - databaseName: "TestDatabase", - ownerUri: ownerUri, - }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Import - expect(endFailedStub).to.have.been.calledOnce; - expect(endStub).to.not.have.been.called; - }); - test("does not include sensitive data in telemetry", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "import-123", - }; - dacFxServiceStub.importBacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ImportBacpacWebviewRequest.type.method); - const params = { - packageFilePath: "C:\\private\\database.bacpac", - databaseName: "PrivateDatabase", - ownerUri: ownerUri, - }; - await requestHandler!(params); - const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry - expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action - expect(endStub.firstCall.args).to.have.lengthOf(1); - }); - }); - suite("Export BACPAC", () => { - test("sends telemetry on successful export", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "export-123", - }; - dacFxServiceStub.exportBacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); - const params = { - databaseName: "SourceDatabase", - packageFilePath: "C:\\backup\\database.bacpac", - ownerUri: ownerUri, - }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Export - expect(startActivityStub.secondCall).to.have.been.calledWith( - TelemetryViews.DacFxApplication, - TelemetryActions.DacFxExportBacpac, - ); - expect(endStub).to.have.been.calledOnce; - expect(endFailedStub).to.not.have.been.called; - }); - test("sends telemetry on export failure", async () => { - const mockResult: DacFxResult = { - success: false, - errorMessage: "Export failed: Insufficient permissions", - operationId: "export-456", - }; - dacFxServiceStub.exportBacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); - const params = { - databaseName: "ProtectedDatabase", - packageFilePath: "C:\\backup\\database.bacpac", - ownerUri: ownerUri, - }; - await requestHandler!(params); - expect(startActivityStub).to.have.been.calledTwice; // Load + Export - expect(endFailedStub).to.have.been.calledOnce; - expect(endStub).to.not.have.been.called; - }); - test("does not include sensitive data in telemetry", async () => { - const mockResult: DacFxResult = { - success: true, - errorMessage: undefined, - operationId: "export-123", - }; - dacFxServiceStub.exportBacpac.resolves(mockResult); - createController(); - const requestHandler = requestHandlers.get(ExportBacpacWebviewRequest.type.method); - const params = { - databaseName: "ConfidentialDatabase", - packageFilePath: "C:\\secure\\database.bacpac", - ownerUri: ownerUri, - }; - await requestHandler!(params); - const startActivityCall = startActivityStub.secondCall; // secondCall is the operation telemetry - expect(startActivityCall.args).to.have.lengthOf(2); // Only view and action - expect(endStub.firstCall.args).to.have.lengthOf(1); - }); - }); - }); }); From ba9709fae70a9b07ff3fbaae7aedea870886c501 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 12:59:53 -0600 Subject: [PATCH 54/79] pr changes to have just one function for filename suggestion --- .../dacFxApplicationWebviewController.ts | 10 ++++++ .../DacFxApplication/dacFxApplicationForm.tsx | 32 ++++++++----------- src/sharedInterfaces/dacFxApplication.ts | 12 +++++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 297feb2ef9..76b1a7a1d0 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -244,6 +244,16 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll }, ); + // Get suggested filename with timestamp + this.onRequest( + dacFxApplication.GetSuggestedFilenameWebviewRequest.type, + async (params: { databaseName: string; fileExtension: string }) => { + const timestamp = this.formatTimestampForFilename(); + const filename = `${params.databaseName}-${timestamp}.${params.fileExtension}`; + return { filename }; + }, + ); + // Get suggested database name from file path this.onRequest( dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index 8ed58ab8c4..69baed402d 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -516,28 +516,22 @@ export const DacFxApplicationForm = () => { ); } else { // Browse for output file (Extract or Export) - // Use the suggested filename from state, or fallback to a default + // Use the suggested filename from state, or get from backend let defaultFileName = filePath; - if (!defaultFileName) { - // Generate default filename with timestamp using Intl.DateTimeFormat - const now = new Date(); - const dateFormatter = new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }); - const timeFormatter = new Intl.DateTimeFormat("en-US", { - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); - - const datePart = dateFormatter.format(now); // yyyy-MM-dd - const timePart = timeFormatter.format(now).replace(/:/g, "-"); // HH-mm - const timestamp = `${datePart}-${timePart}`; + if (!defaultFileName && context?.extensionRpc) { + // Get suggested filename with timestamp from backend + const filenameResult = await context.extensionRpc.sendRequest( + dacFxApplication.GetSuggestedFilenameWebviewRequest.type, + { + databaseName: databaseName || "database", + fileExtension, + }, + ); - defaultFileName = `${databaseName || "database"}-${timestamp}.${fileExtension}`; + if (filenameResult?.filename) { + defaultFileName = filenameResult.filename; + } } result = await context?.extensionRpc?.sendRequest( diff --git a/src/sharedInterfaces/dacFxApplication.ts b/src/sharedInterfaces/dacFxApplication.ts index 53a5ae04ad..17317955e4 100644 --- a/src/sharedInterfaces/dacFxApplication.ts +++ b/src/sharedInterfaces/dacFxApplication.ts @@ -289,6 +289,18 @@ export namespace GetSuggestedOutputPathWebviewRequest { >("dacFxApplication/getSuggestedOutputPath"); } +/** + * Request to get the suggested filename (with timestamp) for an output file + * Used when browsing to suggest a default filename + */ +export namespace GetSuggestedFilenameWebviewRequest { + export const type = new RequestType< + { databaseName: string; fileExtension: string }, + { filename: string }, + void + >("dacFxApplication/getSuggestedFilename"); +} + /** * Request to get the suggested database name from a file path * Extracts database name from the filename without extension or timestamps From 151404a433caff93a7e4eb3570b6687f742a4b2a Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 13:20:29 -0600 Subject: [PATCH 55/79] rename to use a verb per PR comment --- localization/xliff/vscode-mssql.xlf | 2 +- package.json | 6 +++--- package.nls.json | 2 +- src/constants/constants.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 70068f594a..a6e4776c1b 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -4050,7 +4050,7 @@ Create a new table in your database, or edit existing tables with the table designer. Once you're done making your changes, click the 'Publish' button to send the changes to your database. - + Data-tier Application diff --git a/package.json b/package.json index 37807fa531..a4c0233c4b 100644 --- a/package.json +++ b/package.json @@ -548,7 +548,7 @@ "group": "2_MSSQL_serverDbActions@2" }, { - "command": "mssql.dacFxApplication", + "command": "mssql.launchDacFxApplication", "when": "view == objectExplorer && viewItem =~ /\\btype=(disconnectedServer|Server|Database)\\b/", "group": "2_MSSQL_serverDbActions@3" }, @@ -1034,8 +1034,8 @@ "category": "MS SQL" }, { - "command": "mssql.dacFxApplication", - "title": "%mssql.dacFxApplication%", + "command": "mssql.launchDacFxApplication", + "title": "%mssql.launchDacFxApplication%", "category": "MS SQL", "icon": "$(database)" }, diff --git a/package.nls.json b/package.nls.json index a2f82d368b..ab05e303ac 100644 --- a/package.nls.json +++ b/package.nls.json @@ -15,7 +15,7 @@ "mssql.scriptDelete": "Script as Drop", "mssql.scriptExecute": "Script as Execute", "mssql.scriptAlter": "Script as Alter", - "mssql.dacFxApplication": "Data-tier Application", + "mssql.launchDacFxApplication": "Data-tier Application", "mssql.deployDacpac": "Deploy DACPAC", "mssql.extractDacpac": "Extract DACPAC", "mssql.importBacpac": "Import BACPAC", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index af5d44f0e5..1c3d0ac8ed 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -52,7 +52,7 @@ export const cmdNewQuery = "mssql.newQuery"; export const cmdCopilotNewQueryWithConnection = "mssql.copilot.newQueryWithConnection"; export const cmdSchemaCompare = "mssql.schemaCompare"; export const cmdSchemaCompareOpenFromCommandPalette = "mssql.schemaCompareOpenFromCommandPalette"; -export const cmdDacFxApplication = "mssql.dacFxApplication"; +export const cmdDacFxApplication = "mssql.launchDacFxApplication"; export const cmdDeployDacpac = "mssql.deployDacpac"; export const cmdExtractDacpac = "mssql.extractDacpac"; export const cmdImportBacpac = "mssql.importBacpac"; From 1a049b5143f7ad2c1808c2e1fc110555b6529a04 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 13:34:29 -0600 Subject: [PATCH 56/79] refactor to a shared function database validation --- .../dacFxApplicationWebviewController.ts | 43 ++++++++--------- src/models/utils.ts | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacFxApplicationWebviewController.ts index 76b1a7a1d0..7b0342c564 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacFxApplicationWebviewController.ts @@ -22,6 +22,7 @@ import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; import { IConnectionDialogProfile } from "../sharedInterfaces/connectionDialog"; import { getConnectionDisplayName } from "../models/connectionInfo"; +import { validateDatabaseNameFormat, DatabaseNameValidationError } from "../models/utils"; // File extension constants export const DACPAC_EXTENSION = ".dacpac"; @@ -881,28 +882,26 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll shouldNotExist: boolean, operationType?: dacFxApplication.DacFxOperationType, ): Promise<{ isValid: boolean; errorMessage?: string }> { - if (!databaseName || databaseName.trim() === "") { - return { - isValid: false, - errorMessage: LocConstants.DacFxApplication.DatabaseNameRequired, - }; - } - - // Check for invalid characters - const invalidChars = /[<>*?"/\\|]/; - if (invalidChars.test(databaseName)) { - return { - isValid: false, - errorMessage: LocConstants.DacFxApplication.InvalidDatabaseName, - }; - } - - // Check length (SQL Server max identifier length is 128) - if (databaseName.length > 128) { - return { - isValid: false, - errorMessage: LocConstants.DacFxApplication.DatabaseNameTooLong, - }; + // Validate database name format + const formatValidation = validateDatabaseNameFormat(databaseName); + if (!formatValidation.isValid) { + // Map error type to localized message + let errorMessage: string; + switch (formatValidation.errorType) { + case DatabaseNameValidationError.Required: + errorMessage = LocConstants.DacFxApplication.DatabaseNameRequired; + break; + case DatabaseNameValidationError.InvalidCharacters: + errorMessage = LocConstants.DacFxApplication.InvalidDatabaseName; + break; + case DatabaseNameValidationError.TooLong: + errorMessage = LocConstants.DacFxApplication.DatabaseNameTooLong; + break; + default: + errorMessage = LocConstants.DacFxApplication.InvalidDatabaseName; + break; + } + return { isValid: false, errorMessage }; } // Check if database exists diff --git a/src/models/utils.ts b/src/models/utils.ts index 6d946ba1ee..1c05993b76 100644 --- a/src/models/utils.ts +++ b/src/models/utils.ts @@ -771,3 +771,51 @@ export function deepClone(obj: T): T { } export const isLinux = os.platform() === "linux"; + +/** + * Database name validation error types + */ +export enum DatabaseNameValidationError { + None = "None", + Required = "Required", + InvalidCharacters = "InvalidCharacters", + TooLong = "TooLong", +} + +/** + * Validates a database name format according to SQL Server rules. + * Checks for: empty/whitespace, invalid characters, and length limits. + * @param databaseName The database name to validate + * @returns An object with isValid flag and optional error type + */ +export function validateDatabaseNameFormat(databaseName: string): { + isValid: boolean; + errorType?: DatabaseNameValidationError; +} { + // Check for empty or whitespace-only name + if (!databaseName || databaseName.trim() === "") { + return { + isValid: false, + errorType: DatabaseNameValidationError.Required, + }; + } + + // Check for invalid characters + const invalidChars = /[<>*?"/\\|]/; + if (invalidChars.test(databaseName)) { + return { + isValid: false, + errorType: DatabaseNameValidationError.InvalidCharacters, + }; + } + + // Check length (SQL Server max identifier length is 128) + if (databaseName.length > 128) { + return { + isValid: false, + errorType: DatabaseNameValidationError.TooLong, + }; + } + + return { isValid: true, errorType: DatabaseNameValidationError.None }; +} From b5cb330a8edac06d7d42b692d84aaf65bb6f0d8f Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 13:42:24 -0600 Subject: [PATCH 57/79] refactored common logic to a helper function per pr comment --- .../DacFxApplication/dacFxApplicationForm.tsx | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index 69baed402d..67ffee177d 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -395,21 +395,54 @@ export const DacFxApplicationForm = () => { setIsNewDatabase(true); }; + /** + * Helper to determine validation requirements based on operation type + */ + const getValidationRequirements = (opType: dacFxApplication.DacFxOperationType) => { + switch (opType) { + case dacFxApplication.DacFxOperationType.Deploy: + return { filePathShouldExist: true, databaseShouldNotExist: isNewDatabase }; + case dacFxApplication.DacFxOperationType.Extract: + return { filePathShouldExist: false, databaseShouldNotExist: false }; + case dacFxApplication.DacFxOperationType.Import: + return { filePathShouldExist: true, databaseShouldNotExist: true }; + case dacFxApplication.DacFxOperationType.Export: + return { filePathShouldExist: false, databaseShouldNotExist: false }; + } + }; + + /** + * Validates file path and database name based on operation requirements + * @returns true if validation passes, false otherwise + */ + const validateOperationInputs = async ( + opType: dacFxApplication.DacFxOperationType, + ): Promise => { + const requirements = getValidationRequirements(opType); + + const filePathValid = await validateFilePath(filePath, requirements.filePathShouldExist); + const databaseValid = await validateDatabaseName( + databaseName, + requirements.databaseShouldNotExist, + ); + + return filePathValid && databaseValid; + }; + const handleSubmit = async () => { setIsOperationInProgress(true); try { + // Validate inputs before proceeding + if (!(await validateOperationInputs(operationType))) { + setIsOperationInProgress(false); + return; + } + let result; switch (operationType) { case dacFxApplication.DacFxOperationType.Deploy: - if ( - !(await validateFilePath(filePath, true)) || - !(await validateDatabaseName(databaseName, isNewDatabase)) - ) { - setIsOperationInProgress(false); - return; - } result = await context?.extensionRpc?.sendRequest( dacFxApplication.DeployDacpacWebviewRequest.type, { @@ -422,13 +455,6 @@ export const DacFxApplicationForm = () => { break; case dacFxApplication.DacFxOperationType.Extract: - if ( - !(await validateFilePath(filePath, false)) || - !(await validateDatabaseName(databaseName, false)) - ) { - setIsOperationInProgress(false); - return; - } result = await context?.extensionRpc?.sendRequest( dacFxApplication.ExtractDacpacWebviewRequest.type, { @@ -442,13 +468,6 @@ export const DacFxApplicationForm = () => { break; case dacFxApplication.DacFxOperationType.Import: - if ( - !(await validateFilePath(filePath, true)) || - !(await validateDatabaseName(databaseName, true)) - ) { - setIsOperationInProgress(false); - return; - } result = await context?.extensionRpc?.sendRequest( dacFxApplication.ImportBacpacWebviewRequest.type, { @@ -460,13 +479,6 @@ export const DacFxApplicationForm = () => { break; case dacFxApplication.DacFxOperationType.Export: - if ( - !(await validateFilePath(filePath, false)) || - !(await validateDatabaseName(databaseName, false)) - ) { - setIsOperationInProgress(false); - return; - } result = await context?.extensionRpc?.sendRequest( dacFxApplication.ExportBacpacWebviewRequest.type, { From fd7d8cbeb762a4e8e53850b380cb8a09d5c7ae99 Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 15:37:47 -0600 Subject: [PATCH 58/79] changes to centralize the rpc --- .../DacFxApplication/dacFxApplicationForm.tsx | 173 +++++------- .../dacFxApplicationStateProvider.tsx | 250 +++++++++++++++++- 2 files changed, 308 insertions(+), 115 deletions(-) diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx index 67ffee177d..11b296f1ac 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationForm.tsx @@ -76,9 +76,7 @@ export const DacFxApplicationForm = () => { // Cleanup function - cancel ongoing operations when component unmounts return () => { if (isConnecting || isOperationInProgress) { - void context?.extensionRpc?.sendNotification( - dacFxApplication.CancelDacFxApplicationWebviewNotification.type, - ); + void context?.cancel(); } }; }, []); @@ -102,16 +100,13 @@ export const DacFxApplicationForm = () => { databaseName && (operationType === dacFxApplication.DacFxOperationType.Extract || operationType === dacFxApplication.DacFxOperationType.Export) && - context?.extensionRpc + context?.getSuggestedOutputPath ) { // Get the suggested full path from the controller - const result = await context.extensionRpc.sendRequest( - dacFxApplication.GetSuggestedOutputPathWebviewRequest.type, - { - databaseName, - operationType, - }, - ); + const result = await context.getSuggestedOutputPath({ + databaseName, + operationType, + }); if (result?.fullPath) { setFilePath(result.fullPath); @@ -126,15 +121,12 @@ export const DacFxApplicationForm = () => { try { setIsConnecting(true); - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.InitializeConnectionWebviewRequest.type, - { - initialServerName, - initialDatabaseName, - initialOwnerUri, - initialProfileId: initialSelectedProfileId, - }, - ); + const result = await context?.initializeConnection({ + initialServerName, + initialDatabaseName, + initialOwnerUri, + initialProfileId: initialSelectedProfileId, + }); if (result) { // Set all available connections @@ -189,10 +181,7 @@ export const DacFxApplicationForm = () => { try { // Connect to the server - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ConnectToServerWebviewRequest.type, - { profileId }, - ); + const result = await context?.connectToServer({ profileId }); if (result?.isConnected && result.ownerUri) { setOwnerUri(result.ownerUri); @@ -227,10 +216,7 @@ export const DacFxApplicationForm = () => { const loadDatabases = async () => { try { - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ListDatabasesWebviewRequest.type, - { ownerUri: ownerUri || "" }, - ); + const result = await context?.listDatabases({ ownerUri: ownerUri || "" }); if (result?.databases) { setAvailableDatabases(result.databases); } @@ -259,10 +245,7 @@ export const DacFxApplicationForm = () => { } try { - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ValidateFilePathWebviewRequest.type, - { filePath: path, shouldExist }, - ); + const result = await context?.validateFilePath({ filePath: path, shouldExist }); if (!result?.isValid) { setValidationMessages((prev) => ({ @@ -324,15 +307,12 @@ export const DacFxApplicationForm = () => { } try { - const result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ValidateDatabaseNameWebviewRequest.type, - { - databaseName: dbName, - ownerUri: ownerUri || "", - shouldNotExist: shouldNotExist, - operationType: operationType, - }, - ); + const result = await context?.validateDatabaseName({ + databaseName: dbName, + ownerUri: ownerUri || "", + shouldNotExist: shouldNotExist, + operationType: operationType, + }); if (!result?.isValid) { setValidationMessages((prev) => ({ @@ -361,10 +341,7 @@ export const DacFxApplicationForm = () => { operationType === dacFxApplication.DacFxOperationType.Deploy && result.errorMessage === locConstants.dacFxApplication.databaseAlreadyExists ) { - const confirmResult = await context?.extensionRpc?.sendRequest( - dacFxApplication.ConfirmDeployToExistingWebviewRequest.type, - undefined, - ); + const confirmResult = await context?.confirmDeployToExisting(); return confirmResult?.confirmed === true; } @@ -443,50 +420,38 @@ export const DacFxApplicationForm = () => { switch (operationType) { case dacFxApplication.DacFxOperationType.Deploy: - result = await context?.extensionRpc?.sendRequest( - dacFxApplication.DeployDacpacWebviewRequest.type, - { - packageFilePath: filePath, - databaseName, - isNewDatabase, - ownerUri: ownerUri || "", - }, - ); + result = await context?.deployDacpac({ + packageFilePath: filePath, + databaseName, + isNewDatabase, + ownerUri: ownerUri || "", + }); break; case dacFxApplication.DacFxOperationType.Extract: - result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ExtractDacpacWebviewRequest.type, - { - databaseName, - packageFilePath: filePath, - applicationName, - applicationVersion, - ownerUri: ownerUri || "", - }, - ); + result = await context?.extractDacpac({ + databaseName, + packageFilePath: filePath, + applicationName, + applicationVersion, + ownerUri: ownerUri || "", + }); break; case dacFxApplication.DacFxOperationType.Import: - result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ImportBacpacWebviewRequest.type, - { - packageFilePath: filePath, - databaseName, - ownerUri: ownerUri || "", - }, - ); + result = await context?.importBacpac({ + packageFilePath: filePath, + databaseName, + ownerUri: ownerUri || "", + }); break; case dacFxApplication.DacFxOperationType.Export: - result = await context?.extensionRpc?.sendRequest( - dacFxApplication.ExportBacpacWebviewRequest.type, - { - databaseName, - packageFilePath: filePath, - ownerUri: ownerUri || "", - }, - ); + result = await context?.exportBacpac({ + databaseName, + packageFilePath: filePath, + ownerUri: ownerUri || "", + }); break; } @@ -520,39 +485,30 @@ export const DacFxApplicationForm = () => { if (requiresInputFile) { // Browse for input file (Deploy or Import) - result = await context?.extensionRpc?.sendRequest( - dacFxApplication.BrowseInputFileWebviewRequest.type, - { - fileExtension, - }, - ); + result = await context?.browseInputFile({ + fileExtension, + }); } else { // Browse for output file (Extract or Export) // Use the suggested filename from state, or get from backend let defaultFileName = filePath; - if (!defaultFileName && context?.extensionRpc) { + if (!defaultFileName && context) { // Get suggested filename with timestamp from backend - const filenameResult = await context.extensionRpc.sendRequest( - dacFxApplication.GetSuggestedFilenameWebviewRequest.type, - { - databaseName: databaseName || "database", - fileExtension, - }, - ); + const filenameResult = await context.getSuggestedFilename({ + databaseName: databaseName || "database", + fileExtension, + }); if (filenameResult?.filename) { defaultFileName = filenameResult.filename; } } - result = await context?.extensionRpc?.sendRequest( - dacFxApplication.BrowseOutputFileWebviewRequest.type, - { - fileExtension, - defaultFileName, - }, - ); + result = await context?.browseOutputFile({ + fileExtension, + defaultFileName, + }); } if (result?.filePath) { @@ -567,16 +523,13 @@ export const DacFxApplicationForm = () => { // For Deploy/Import operations, suggest database name from the selected file if ( requiresInputFile && - context?.extensionRpc && + context && (operationType === dacFxApplication.DacFxOperationType.Deploy || operationType === dacFxApplication.DacFxOperationType.Import) ) { - const nameResult = await context.extensionRpc.sendRequest( - dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, - { - filePath: result.filePath, - }, - ); + const nameResult = await context.getSuggestedDatabaseName({ + filePath: result.filePath, + }); if (nameResult?.databaseName) { setDatabaseName(nameResult.databaseName); @@ -590,9 +543,7 @@ export const DacFxApplicationForm = () => { }; const handleCancel = async () => { - await context?.extensionRpc?.sendNotification( - dacFxApplication.CancelDacFxApplicationWebviewNotification.type, - ); + await context?.cancel(); }; const isFormValid = () => { diff --git a/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx b/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx index 5c4237bd65..914a89b779 100644 --- a/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx +++ b/src/reactviews/pages/DacFxApplication/dacFxApplicationStateProvider.tsx @@ -4,11 +4,92 @@ *--------------------------------------------------------------------------------------------*/ import React, { createContext, ReactNode } from "react"; -import { DacFxApplicationWebviewState } from "../../../sharedInterfaces/dacFxApplication"; +import * as dacFxApplication from "../../../sharedInterfaces/dacFxApplication"; +import { IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog"; import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2"; import { WebviewRpc } from "../../common/rpc"; -export interface DacFxApplicationReactProvider { +/** + * RPC helper methods for DacFx operations + */ +export interface DacFxApplicationRpcMethods { + // Operation execution methods + deployDacpac: ( + params: dacFxApplication.DeployDacpacParams, + ) => Promise; + extractDacpac: ( + params: dacFxApplication.ExtractDacpacParams, + ) => Promise; + importBacpac: ( + params: dacFxApplication.ImportBacpacParams, + ) => Promise; + exportBacpac: ( + params: dacFxApplication.ExportBacpacParams, + ) => Promise; + + // Validation methods + validateFilePath: (params: { + filePath: string; + shouldExist: boolean; + }) => Promise<{ isValid: boolean; errorMessage?: string } | undefined>; + validateDatabaseName: (params: { + databaseName: string; + ownerUri: string; + shouldNotExist: boolean; + operationType?: dacFxApplication.DacFxOperationType; + }) => Promise<{ isValid: boolean; errorMessage?: string } | undefined>; + + // Connection methods + initializeConnection: (params: { + initialServerName?: string; + initialDatabaseName?: string; + initialOwnerUri?: string; + initialProfileId?: string; + }) => Promise< + | { + connections: IConnectionDialogProfile[]; + selectedConnection?: IConnectionDialogProfile; + ownerUri?: string; + autoConnected: boolean; + errorMessage?: string; + } + | undefined + >; + connectToServer: (params: { + profileId: string; + }) => Promise<{ ownerUri: string; isConnected: boolean; errorMessage?: string } | undefined>; + listDatabases: (params: { ownerUri: string }) => Promise<{ databases: string[] } | undefined>; + + // File browsing methods + browseInputFile: (params: { + fileExtension: string; + }) => Promise<{ filePath?: string } | undefined>; + browseOutputFile: (params: { + fileExtension: string; + defaultFileName?: string; + }) => Promise<{ filePath?: string } | undefined>; + + // Helper methods + getSuggestedOutputPath: (params: { + databaseName: string; + operationType: dacFxApplication.DacFxOperationType; + }) => Promise<{ fullPath: string } | undefined>; + getSuggestedFilename: (params: { + databaseName: string; + fileExtension: string; + }) => Promise<{ filename: string } | undefined>; + getSuggestedDatabaseName: (params: { + filePath: string; + }) => Promise<{ databaseName: string } | undefined>; + + // Confirmation dialog + confirmDeployToExisting: () => Promise<{ confirmed: boolean } | undefined>; + + // Cancel operation + cancel: () => Promise; +} + +export interface DacFxApplicationReactProvider extends DacFxApplicationRpcMethods { extensionRpc: WebviewRpc; } @@ -21,9 +102,170 @@ interface DacFxApplicationProviderProps { } const DacFxApplicationStateProvider: React.FC = ({ children }) => { - const { extensionRpc } = useVscodeWebview2(); + const { extensionRpc } = useVscodeWebview2< + dacFxApplication.DacFxApplicationWebviewState, + void + >(); + + // Operation execution methods + const deployDacpac = async (params: dacFxApplication.DeployDacpacParams) => { + return await extensionRpc?.sendRequest( + dacFxApplication.DeployDacpacWebviewRequest.type, + params, + ); + }; + + const extractDacpac = async (params: dacFxApplication.ExtractDacpacParams) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ExtractDacpacWebviewRequest.type, + params, + ); + }; + + const importBacpac = async (params: dacFxApplication.ImportBacpacParams) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ImportBacpacWebviewRequest.type, + params, + ); + }; + + const exportBacpac = async (params: dacFxApplication.ExportBacpacParams) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ExportBacpacWebviewRequest.type, + params, + ); + }; + + // Validation methods + const validateFilePath = async (params: { filePath: string; shouldExist: boolean }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ValidateFilePathWebviewRequest.type, + params, + ); + }; + + const validateDatabaseName = async (params: { + databaseName: string; + ownerUri: string; + shouldNotExist: boolean; + operationType?: dacFxApplication.DacFxOperationType; + }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ValidateDatabaseNameWebviewRequest.type, + params, + ); + }; + + // Connection methods + const initializeConnection = async (params: { + initialServerName?: string; + initialDatabaseName?: string; + initialOwnerUri?: string; + initialProfileId?: string; + }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.InitializeConnectionWebviewRequest.type, + params, + ); + }; + + const connectToServer = async (params: { profileId: string }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ConnectToServerWebviewRequest.type, + params, + ); + }; + + const listDatabases = async (params: { ownerUri: string }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.ListDatabasesWebviewRequest.type, + params, + ); + }; + + // File browsing methods + const browseInputFile = async (params: { fileExtension: string }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.BrowseInputFileWebviewRequest.type, + params, + ); + }; + + const browseOutputFile = async (params: { + fileExtension: string; + defaultFileName?: string; + }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.BrowseOutputFileWebviewRequest.type, + params, + ); + }; + + // Helper methods + const getSuggestedOutputPath = async (params: { + databaseName: string; + operationType: dacFxApplication.DacFxOperationType; + }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.GetSuggestedOutputPathWebviewRequest.type, + params, + ); + }; + + const getSuggestedFilename = async (params: { + databaseName: string; + fileExtension: string; + }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.GetSuggestedFilenameWebviewRequest.type, + params, + ); + }; + + const getSuggestedDatabaseName = async (params: { filePath: string }) => { + return await extensionRpc?.sendRequest( + dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, + params, + ); + }; + + // Confirmation dialog + const confirmDeployToExisting = async () => { + return await extensionRpc?.sendRequest( + dacFxApplication.ConfirmDeployToExistingWebviewRequest.type, + undefined, + ); + }; + + // Cancel operation + const cancel = async () => { + await extensionRpc?.sendNotification( + dacFxApplication.CancelDacFxApplicationWebviewNotification.type, + ); + }; + + const providerValue: DacFxApplicationReactProvider = { + extensionRpc, + deployDacpac, + extractDacpac, + importBacpac, + exportBacpac, + validateFilePath, + validateDatabaseName, + initializeConnection, + connectToServer, + listDatabases, + browseInputFile, + browseOutputFile, + getSuggestedOutputPath, + getSuggestedFilename, + getSuggestedDatabaseName, + confirmDeployToExisting, + cancel, + }; + return ( - + {children} ); From 5fe574590acd9dc80a3f04c515e449c94790da6f Mon Sep 17 00:00:00 2001 From: allancascante Date: Thu, 6 Nov 2025 16:50:51 -0600 Subject: [PATCH 59/79] rename for the entire functionality --- localization/xliff/vscode-mssql.xlf | 2 +- package.json | 6 +- package.nls.json | 2 +- rename_script.ps1 | 27 ++++ scripts/bundle-reactviews.js | 2 +- src/constants/constants.ts | 2 +- src/constants/locConstants.ts | 2 +- ...er.ts => dacpacDialogWebviewController.ts} | 140 +++++++++--------- src/controllers/mainController.ts | 13 +- src/reactviews/common/locConstants.ts | 4 +- .../ApplicationInfoSection.tsx | 10 +- .../FilePathSection.tsx | 16 +- .../OperationTypeSection.tsx | 30 ++-- .../ServerSelectionSection.tsx | 10 +- .../SourceDatabaseSection.tsx | 12 +- .../TargetDatabaseSection.tsx | 24 +-- .../dacpacDialog.css} | 0 .../dacpacDialogForm.tsx} | 132 ++++++++--------- .../dacpacDialogPage.tsx} | 12 +- .../dacpacDialogSelector.ts} | 8 +- .../dacpacDialogStateProvider.tsx} | 91 ++++++------ .../index.tsx | 10 +- .../components/SchemaSelectorDrawer.tsx | 2 +- .../{dacFxApplication.ts => dacpacDialog.ts} | 52 +++---- src/sharedInterfaces/telemetry.ts | 2 +- ... => dacpacDialogWebviewController.test.ts} | 76 ++++------ test/unit/mainController.test.ts | 4 +- 27 files changed, 342 insertions(+), 349 deletions(-) create mode 100644 rename_script.ps1 rename src/controllers/{dacFxApplicationWebviewController.ts => dacpacDialogWebviewController.ts} (87%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/ApplicationInfoSection.tsx (81%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/FilePathSection.tsx (82%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/OperationTypeSection.tsx (69%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/ServerSelectionSection.tsx (90%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/SourceDatabaseSection.tsx (86%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/TargetDatabaseSection.tsx (80%) rename src/reactviews/pages/{DacFxApplication/dacFxApplication.css => DacpacDialog/dacpacDialog.css} (100%) rename src/reactviews/pages/{DacFxApplication/dacFxApplicationForm.tsx => DacpacDialog/dacpacDialogForm.tsx} (82%) rename src/reactviews/pages/{DacFxApplication/dacFxApplicationPage.tsx => DacpacDialog/dacpacDialogPage.tsx} (73%) rename src/reactviews/pages/{DacFxApplication/dacFxApplicationSelector.ts => DacpacDialog/dacpacDialogSelector.ts} (62%) rename src/reactviews/pages/{DacFxApplication/dacFxApplicationStateProvider.tsx => DacpacDialog/dacpacDialogStateProvider.tsx} (67%) rename src/reactviews/pages/{DacFxApplication => DacpacDialog}/index.tsx (70%) rename src/sharedInterfaces/{dacFxApplication.ts => dacpacDialog.ts} (84%) rename test/unit/{dacFxApplicationWebviewController.test.ts => dacpacDialogWebviewController.test.ts} (97%) diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index a6e4776c1b..4a5ba4ac84 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -4050,7 +4050,7 @@ Create a new table in your database, or edit existing tables with the table designer. Once you're done making your changes, click the 'Publish' button to send the changes to your database. - + Data-tier Application diff --git a/package.json b/package.json index a4c0233c4b..ad80765c97 100644 --- a/package.json +++ b/package.json @@ -548,7 +548,7 @@ "group": "2_MSSQL_serverDbActions@2" }, { - "command": "mssql.launchDacFxApplication", + "command": "mssql.launchDacpacDialog", "when": "view == objectExplorer && viewItem =~ /\\btype=(disconnectedServer|Server|Database)\\b/", "group": "2_MSSQL_serverDbActions@3" }, @@ -1034,8 +1034,8 @@ "category": "MS SQL" }, { - "command": "mssql.launchDacFxApplication", - "title": "%mssql.launchDacFxApplication%", + "command": "mssql.launchDacpacDialog", + "title": "%mssql.launchDacpacDialog%", "category": "MS SQL", "icon": "$(database)" }, diff --git a/package.nls.json b/package.nls.json index ab05e303ac..b2ab83f750 100644 --- a/package.nls.json +++ b/package.nls.json @@ -15,7 +15,7 @@ "mssql.scriptDelete": "Script as Drop", "mssql.scriptExecute": "Script as Execute", "mssql.scriptAlter": "Script as Alter", - "mssql.launchDacFxApplication": "Data-tier Application", + "mssql.launchDacpacDialog": "Data-tier Application", "mssql.deployDacpac": "Deploy DACPAC", "mssql.extractDacpac": "Extract DACPAC", "mssql.importBacpac": "Import BACPAC", diff --git a/rename_script.ps1 b/rename_script.ps1 new file mode 100644 index 0000000000..85a7e885f7 --- /dev/null +++ b/rename_script.ps1 @@ -0,0 +1,27 @@ +# Rename dacFxApplication to dacpacDialog +# This script will be used to systematically rename all occurrences + +$ErrorActionPreference = "Stop" + +# Define file paths relative to vscode-mssql root +$rootPath = "c:\vscode-mssql" + +# Step 1: Rename files +Write-Host "Step 1: Renaming files..." -ForegroundColor Cyan + +$fileRenames = @( + @{Old = "src\sharedInterfaces\dacFxApplication.ts"; New = "src\sharedInterfaces\dacpacDialog.ts"}, + @{Old = "src\controllers\dacFxApplicationWebviewController.ts"; New = "src\controllers\dacpacDialogWebviewController.ts"}, + @{Old = "src\reactviews\pages\DacFxApplication\dacFxApplicationForm.tsx"; New = "src\reactviews\pages\DacpacDialog\dacpacDialogForm.tsx"}, + @{Old = "src\reactviews\pages\DacFxApplication\dacFxApplicationPage.tsx"; New = "src\reactviews\pages\DacpacDialog\dacpacDialogPage.tsx"}, + @{Old = "src\reactviews\pages\DacFxApplication\dacFxApplicationStateProvider.tsx"; New = "src\reactviews\pages\DacpacDialog\dacpacDialogStateProvider.tsx"}, + @{Old = "src\reactviews\pages\DacFxApplication\dacFxApplicationSelector.ts"; New = "src\reactviews\pages\DacpacDialog\dacpacDialogSelector.ts"}, + @{Old = "src\reactviews\pages\DacFxApplication\dacFxApplication.css"; New = "src\reactviews\pages\DacpacDialog\dacpacDialog.css"}, + @{Old = "test\unit\dacFxApplicationWebviewController.test.ts"; New = "test\unit\dacpacDialogWebviewController.test.ts"} +) + +# Also need to rename the directory +Write-Host "Renaming directory DacFxApplication to DacpacDialog..." + +Write-Host "`nTotal files to rename: $($fileRenames.Count + 1)" +Write-Host "This script is ready. Should I proceed with the renames?" diff --git a/scripts/bundle-reactviews.js b/scripts/bundle-reactviews.js index acdcfafdaf..1844fa1a51 100644 --- a/scripts/bundle-reactviews.js +++ b/scripts/bundle-reactviews.js @@ -17,7 +17,7 @@ const config = { addFirewallRule: "src/reactviews/pages/AddFirewallRule/index.tsx", connectionDialog: "src/reactviews/pages/ConnectionDialog/index.tsx", connectionGroup: "src/reactviews/pages/ConnectionGroup/index.tsx", - dacFxApplication: "src/reactviews/pages/DacFxApplication/index.tsx", + DacpacDialog: "src/reactviews/pages/DacpacDialog/index.tsx", deployment: "src/reactviews/pages/Deployment/index.tsx", executionPlan: "src/reactviews/pages/ExecutionPlan/index.tsx", tableDesigner: "src/reactviews/pages/TableDesigner/index.tsx", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 1c3d0ac8ed..d705af58a9 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -52,7 +52,7 @@ export const cmdNewQuery = "mssql.newQuery"; export const cmdCopilotNewQueryWithConnection = "mssql.copilot.newQueryWithConnection"; export const cmdSchemaCompare = "mssql.schemaCompare"; export const cmdSchemaCompareOpenFromCommandPalette = "mssql.schemaCompareOpenFromCommandPalette"; -export const cmdDacFxApplication = "mssql.launchDacFxApplication"; +export const cmdDacpacDialog = "mssql.launchDacpacDialog"; export const cmdDeployDacpac = "mssql.deployDacpac"; export const cmdExtractDacpac = "mssql.extractDacpac"; export const cmdImportBacpac = "mssql.importBacpac"; diff --git a/src/constants/locConstants.ts b/src/constants/locConstants.ts index 6f7c55ed5e..bb82253ff2 100644 --- a/src/constants/locConstants.ts +++ b/src/constants/locConstants.ts @@ -2073,7 +2073,7 @@ export class ConnectionGroup { }; } -export class DacFxApplication { +export class DacpacDialog { public static Title = l10n.t("Data-tier Application"); public static FilePathRequired = l10n.t("File path is required"); public static FileNotFound = l10n.t("File not found"); diff --git a/src/controllers/dacFxApplicationWebviewController.ts b/src/controllers/dacpacDialogWebviewController.ts similarity index 87% rename from src/controllers/dacFxApplicationWebviewController.ts rename to src/controllers/dacpacDialogWebviewController.ts index 7b0342c564..7f099c238a 100644 --- a/src/controllers/dacFxApplicationWebviewController.ts +++ b/src/controllers/dacpacDialogWebviewController.ts @@ -17,7 +17,7 @@ import VscodeWrapper from "./vscodeWrapper"; import * as LocConstants from "../constants/locConstants"; import { startActivity } from "../telemetry/telemetry"; import { TelemetryViews, TelemetryActions, ActivityStatus } from "../sharedInterfaces/telemetry"; -import * as dacFxApplication from "../sharedInterfaces/dacFxApplication"; +import * as dacpacDialog from "../sharedInterfaces/dacpacDialog"; import { TaskExecutionMode } from "../sharedInterfaces/schemaCompare"; import { ListDatabasesRequest } from "../models/contracts/connection"; import { IConnectionDialogProfile } from "../sharedInterfaces/connectionDialog"; @@ -29,13 +29,13 @@ export const DACPAC_EXTENSION = ".dacpac"; export const BACPAC_EXTENSION = ".bacpac"; /** - * Controller for the DacFxApplication webview. + * Controller for the DacpacDialog webview. * Manages DACPAC and BACPAC operations (Deploy, Extract, Import, Export) using the Data-tier Application Framework (DacFx). */ -export class DacFxApplicationWebviewController extends ReactWebviewPanelController< - dacFxApplication.DacFxApplicationWebviewState, +export class DacpacDialogWebviewController extends ReactWebviewPanelController< + dacpacDialog.DacpacDialogWebviewState, void, - dacFxApplication.DacFxApplicationResult + dacpacDialog.DacpacDialogResult > { private _ownerUri: string; @@ -44,11 +44,11 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll vscodeWrapper: VscodeWrapper, private connectionManager: ConnectionManager, private dacFxService: DacFxService, - initialState: dacFxApplication.DacFxApplicationWebviewState, + initialState: dacpacDialog.DacpacDialogWebviewState, ownerUri: string, ) { - super(context, vscodeWrapper, "dacFxApplication", "dacFxApplication", initialState, { - title: LocConstants.DacFxApplication.Title, + super(context, vscodeWrapper, "dacpacDialog", "dacpacDialog", initialState, { + title: LocConstants.DacpacDialog.Title, viewColumn: vscode.ViewColumn.Active, iconPath: { dark: vscode.Uri.joinPath(context.extensionUri, "media", "database_dark.svg"), @@ -67,39 +67,39 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll private registerRpcHandlers(): void { // Deploy DACPAC request handler this.onRequest( - dacFxApplication.DeployDacpacWebviewRequest.type, - async (params: dacFxApplication.DeployDacpacParams) => { + dacpacDialog.DeployDacpacWebviewRequest.type, + async (params: dacpacDialog.DeployDacpacParams) => { return await this.handleDeployDacpac(params); }, ); // Extract DACPAC request handler this.onRequest( - dacFxApplication.ExtractDacpacWebviewRequest.type, - async (params: dacFxApplication.ExtractDacpacParams) => { + dacpacDialog.ExtractDacpacWebviewRequest.type, + async (params: dacpacDialog.ExtractDacpacParams) => { return await this.handleExtractDacpac(params); }, ); // Import BACPAC request handler this.onRequest( - dacFxApplication.ImportBacpacWebviewRequest.type, - async (params: dacFxApplication.ImportBacpacParams) => { + dacpacDialog.ImportBacpacWebviewRequest.type, + async (params: dacpacDialog.ImportBacpacParams) => { return await this.handleImportBacpac(params); }, ); // Export BACPAC request handler this.onRequest( - dacFxApplication.ExportBacpacWebviewRequest.type, - async (params: dacFxApplication.ExportBacpacParams) => { + dacpacDialog.ExportBacpacWebviewRequest.type, + async (params: dacpacDialog.ExportBacpacParams) => { return await this.handleExportBacpac(params); }, ); // Validate file path request handler this.onRequest( - dacFxApplication.ValidateFilePathWebviewRequest.type, + dacpacDialog.ValidateFilePathWebviewRequest.type, async (params: { filePath: string; shouldExist: boolean }) => { return this.validateFilePath(params.filePath, params.shouldExist); }, @@ -107,7 +107,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // List databases request handler this.onRequest( - dacFxApplication.ListDatabasesWebviewRequest.type, + dacpacDialog.ListDatabasesWebviewRequest.type, async (params: { ownerUri: string }) => { if (!params.ownerUri || params.ownerUri.trim() === "") { this.logger.error("Cannot list databases: ownerUri is empty"); @@ -119,12 +119,12 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Validate database name request handler this.onRequest( - dacFxApplication.ValidateDatabaseNameWebviewRequest.type, + dacpacDialog.ValidateDatabaseNameWebviewRequest.type, async (params: { databaseName: string; ownerUri: string; shouldNotExist: boolean; - operationType?: dacFxApplication.DacFxOperationType; + operationType?: dacpacDialog.DacFxOperationType; }) => { if (!params.ownerUri || params.ownerUri.trim() === "") { this.logger.error("Cannot validate database name: ownerUri is empty"); @@ -144,13 +144,13 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll ); // List connections request handler - this.onRequest(dacFxApplication.ListConnectionsWebviewRequest.type, async () => { + this.onRequest(dacpacDialog.ListConnectionsWebviewRequest.type, async () => { return await this.listConnections(); }); // Initialize connection request handler this.onRequest( - dacFxApplication.InitializeConnectionWebviewRequest.type, + dacpacDialog.InitializeConnectionWebviewRequest.type, async (params: { initialServerName?: string; initialDatabaseName?: string; @@ -163,7 +163,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Connect to server request handler this.onRequest( - dacFxApplication.ConnectToServerWebviewRequest.type, + dacpacDialog.ConnectToServerWebviewRequest.type, async (params: { profileId: string }) => { return await this.connectToServer(params.profileId); }, @@ -171,15 +171,15 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Browse for input file (DACPAC or BACPAC) request handler this.onRequest( - dacFxApplication.BrowseInputFileWebviewRequest.type, + dacpacDialog.BrowseInputFileWebviewRequest.type, async (params: { fileExtension: string }) => { const fileUri = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - openLabel: LocConstants.DacFxApplication.Select, + openLabel: LocConstants.DacpacDialog.Select, filters: { - [`${params.fileExtension.toUpperCase()} ${LocConstants.DacFxApplication.Files}`]: + [`${params.fileExtension.toUpperCase()} ${LocConstants.DacpacDialog.Files}`]: [params.fileExtension], }, }); @@ -194,7 +194,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Browse for output file (DACPAC or BACPAC) request handler this.onRequest( - dacFxApplication.BrowseOutputFileWebviewRequest.type, + dacpacDialog.BrowseOutputFileWebviewRequest.type, async (params: { fileExtension: string; defaultFileName?: string }) => { const defaultFileName = params.defaultFileName || `database.${params.fileExtension}`; @@ -205,9 +205,9 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll const fileUri = await vscode.window.showSaveDialog({ defaultUri: defaultUri, - saveLabel: LocConstants.DacFxApplication.Save, + saveLabel: LocConstants.DacpacDialog.Save, filters: { - [`${params.fileExtension.toUpperCase()} ${LocConstants.DacFxApplication.Files}`]: + [`${params.fileExtension.toUpperCase()} ${LocConstants.DacpacDialog.Files}`]: [params.fileExtension], }, }); @@ -222,13 +222,13 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get default output path without showing dialog this.onRequest( - dacFxApplication.GetSuggestedOutputPathWebviewRequest.type, + dacpacDialog.GetSuggestedOutputPathWebviewRequest.type, async (params: { databaseName: string; - operationType: dacFxApplication.DacFxOperationType; + operationType: dacpacDialog.DacFxOperationType; }) => { const fileExtension = - params.operationType === dacFxApplication.DacFxOperationType.Extract + params.operationType === dacpacDialog.DacFxOperationType.Extract ? "dacpac" : "bacpac"; @@ -247,7 +247,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get suggested filename with timestamp this.onRequest( - dacFxApplication.GetSuggestedFilenameWebviewRequest.type, + dacpacDialog.GetSuggestedFilenameWebviewRequest.type, async (params: { databaseName: string; fileExtension: string }) => { const timestamp = this.formatTimestampForFilename(); const filename = `${params.databaseName}-${timestamp}.${params.fileExtension}`; @@ -257,7 +257,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // Get suggested database name from file path this.onRequest( - dacFxApplication.GetSuggestedDatabaseNameWebviewRequest.type, + dacpacDialog.GetSuggestedDatabaseNameWebviewRequest.type, async (params: { filePath: string }) => { // Remove file extension (.dacpac or .bacpac) to get the database name // Keep the full filename including any timestamps that may be present @@ -268,20 +268,20 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll ); // Confirm deploy to existing database request handler - this.onRequest(dacFxApplication.ConfirmDeployToExistingWebviewRequest.type, async () => { + this.onRequest(dacpacDialog.ConfirmDeployToExistingWebviewRequest.type, async () => { const result = await this.vscodeWrapper.showWarningMessageAdvanced( - LocConstants.DacFxApplication.DeployToExistingMessage, + LocConstants.DacpacDialog.DeployToExistingMessage, { modal: true }, - [LocConstants.DacFxApplication.DeployToExistingConfirm], + [LocConstants.DacpacDialog.DeployToExistingConfirm], ); return { - confirmed: result === LocConstants.DacFxApplication.DeployToExistingConfirm, + confirmed: result === LocConstants.DacpacDialog.DeployToExistingConfirm, }; }); // Cancel operation notification handler - this.onNotification(dacFxApplication.CancelDacFxApplicationWebviewNotification.type, () => { + this.onNotification(dacpacDialog.CancelDacpacDialogWebviewNotification.type, () => { this.dialogResult.resolve(undefined); this.panel.dispose(); }); @@ -291,10 +291,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Handles deploying a DACPAC file to a database */ private async handleDeployDacpac( - params: dacFxApplication.DeployDacpacParams, - ): Promise { + params: dacpacDialog.DeployDacpacParams, + ): Promise { const activity = startActivity( - TelemetryViews.DacFxApplication, + TelemetryViews.DacpacDialog, TelemetryActions.DacFxDeployDacpac, undefined, { @@ -311,7 +311,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: dacFxApplication.DacFxApplicationResult = { + const appResult: dacpacDialog.DacpacDialogResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -343,10 +343,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Handles extracting a DACPAC file from a database */ private async handleExtractDacpac( - params: dacFxApplication.ExtractDacpacParams, - ): Promise { + params: dacpacDialog.ExtractDacpacParams, + ): Promise { const activity = startActivity( - TelemetryViews.DacFxApplication, + TelemetryViews.DacpacDialog, TelemetryActions.DacFxExtractDacpac, ); @@ -360,7 +360,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: dacFxApplication.DacFxApplicationResult = { + const appResult: dacpacDialog.DacpacDialogResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -392,10 +392,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Handles importing a BACPAC file to create a new database */ private async handleImportBacpac( - params: dacFxApplication.ImportBacpacParams, - ): Promise { + params: dacpacDialog.ImportBacpacParams, + ): Promise { const activity = startActivity( - TelemetryViews.DacFxApplication, + TelemetryViews.DacpacDialog, TelemetryActions.DacFxImportBacpac, ); @@ -407,7 +407,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: dacFxApplication.DacFxApplicationResult = { + const appResult: dacpacDialog.DacpacDialogResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -439,10 +439,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll * Handles exporting a database to a BACPAC file */ private async handleExportBacpac( - params: dacFxApplication.ExportBacpacParams, - ): Promise { + params: dacpacDialog.ExportBacpacParams, + ): Promise { const activity = startActivity( - TelemetryViews.DacFxApplication, + TelemetryViews.DacpacDialog, TelemetryActions.DacFxExportBacpac, ); @@ -454,7 +454,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll TaskExecutionMode.execute, ); - const appResult: dacFxApplication.DacFxApplicationResult = { + const appResult: dacpacDialog.DacpacDialogResult = { success: result.success, errorMessage: result.errorMessage, operationId: result.operationId, @@ -492,7 +492,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll if (!filePath || filePath.trim() === "") { return { isValid: false, - errorMessage: LocConstants.DacFxApplication.FilePathRequired, + errorMessage: LocConstants.DacpacDialog.FilePathRequired, }; } @@ -501,7 +501,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll if (shouldExist && !fileFound) { return { isValid: false, - errorMessage: LocConstants.DacFxApplication.FileNotFound, + errorMessage: LocConstants.DacpacDialog.FileNotFound, }; } @@ -509,7 +509,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll if (extension !== DACPAC_EXTENSION && extension !== BACPAC_EXTENSION) { return { isValid: false, - errorMessage: LocConstants.DacFxApplication.InvalidFileExtension, + errorMessage: LocConstants.DacpacDialog.InvalidFileExtension, }; } @@ -519,7 +519,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll if (!fs.existsSync(directory)) { return { isValid: false, - errorMessage: LocConstants.DacFxApplication.DirectoryNotFound, + errorMessage: LocConstants.DacpacDialog.DirectoryNotFound, }; } @@ -528,7 +528,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // This is just a warning - the operation can continue with user confirmation return { isValid: true, - errorMessage: LocConstants.DacFxApplication.FileAlreadyExists, + errorMessage: LocConstants.DacpacDialog.FileAlreadyExists, }; } } @@ -880,7 +880,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll databaseName: string, ownerUri: string, shouldNotExist: boolean, - operationType?: dacFxApplication.DacFxOperationType, + operationType?: dacpacDialog.DacFxOperationType, ): Promise<{ isValid: boolean; errorMessage?: string }> { // Validate database name format const formatValidation = validateDatabaseNameFormat(databaseName); @@ -889,16 +889,16 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll let errorMessage: string; switch (formatValidation.errorType) { case DatabaseNameValidationError.Required: - errorMessage = LocConstants.DacFxApplication.DatabaseNameRequired; + errorMessage = LocConstants.DacpacDialog.DatabaseNameRequired; break; case DatabaseNameValidationError.InvalidCharacters: - errorMessage = LocConstants.DacFxApplication.InvalidDatabaseName; + errorMessage = LocConstants.DacpacDialog.InvalidDatabaseName; break; case DatabaseNameValidationError.TooLong: - errorMessage = LocConstants.DacFxApplication.DatabaseNameTooLong; + errorMessage = LocConstants.DacpacDialog.DatabaseNameTooLong; break; default: - errorMessage = LocConstants.DacFxApplication.InvalidDatabaseName; + errorMessage = LocConstants.DacpacDialog.InvalidDatabaseName; break; } return { isValid: false, errorMessage }; @@ -918,10 +918,10 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll // This ensures confirmation dialog is shown in both cases: // 1. User selected "New Database" but database already exists (shouldNotExist=true) // 2. User selected "Existing Database" and selected existing database (shouldNotExist=false) - if (operationType === dacFxApplication.DacFxOperationType.Deploy && exists) { + if (operationType === dacpacDialog.DacFxOperationType.Deploy && exists) { return { isValid: true, // Allow the operation but with a warning - errorMessage: LocConstants.DacFxApplication.DatabaseAlreadyExists, + errorMessage: LocConstants.DacpacDialog.DatabaseAlreadyExists, }; } @@ -929,7 +929,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll if (shouldNotExist && exists) { return { isValid: true, // Allow the operation but with a warning - errorMessage: LocConstants.DacFxApplication.DatabaseAlreadyExists, + errorMessage: LocConstants.DacpacDialog.DatabaseAlreadyExists, }; } @@ -937,7 +937,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll if (!shouldNotExist && !exists) { return { isValid: false, - errorMessage: LocConstants.DacFxApplication.DatabaseNotFound, + errorMessage: LocConstants.DacpacDialog.DatabaseNotFound, }; } @@ -946,7 +946,7 @@ export class DacFxApplicationWebviewController extends ReactWebviewPanelControll const errorMessage = error instanceof Error ? `Failed to validate database name: ${error.message}` - : LocConstants.DacFxApplication.ValidationFailed; + : LocConstants.DacpacDialog.ValidationFailed; this.logger.error(errorMessage); return { isValid: false, diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 2cab3afb18..cb00b1592f 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -47,11 +47,8 @@ import { ActivityStatus, TelemetryActions, TelemetryViews } from "../sharedInter import { TableDesignerService } from "../services/tableDesignerService"; import { TableDesignerWebviewController } from "../tableDesigner/tableDesignerWebviewController"; import { ConnectionDialogWebviewController } from "../connectionconfig/connectionDialogWebviewController"; -import { DacFxApplicationWebviewController } from "./dacFxApplicationWebviewController"; -import { - DacFxApplicationWebviewState, - DacFxOperationType, -} from "../sharedInterfaces/dacFxApplication"; +import { DacpacDialogWebviewController } from "./dacpacDialogWebviewController"; +import { DacpacDialogWebviewState, DacFxOperationType } from "../sharedInterfaces/dacpacDialog"; import { ObjectExplorerFilter } from "../objectExplorer/objectExplorerFilter"; import { DatabaseObjectSearchService, @@ -1808,7 +1805,7 @@ export default class MainController implements vscode.Disposable { `${connectionProfile.server}_${connectionProfile.database || ""}` : undefined; - const initialState: DacFxApplicationWebviewState = { + const initialState: DacpacDialogWebviewState = { ownerUri, serverName, databaseName, @@ -1816,7 +1813,7 @@ export default class MainController implements vscode.Disposable { operationType, }; - const controller = new DacFxApplicationWebviewController( + const controller = new DacpacDialogWebviewController( this._context, this._vscodeWrapper, this._connectionMgr, @@ -1830,7 +1827,7 @@ export default class MainController implements vscode.Disposable { }; // Data-tier Application commands - registerDacFxCommand(Constants.cmdDacFxApplication, DacFxOperationType.Deploy); + registerDacFxCommand(Constants.cmdDacpacDialog, DacFxOperationType.Deploy); registerDacFxCommand(Constants.cmdDeployDacpac, DacFxOperationType.Deploy); registerDacFxCommand(Constants.cmdExtractDacpac, DacFxOperationType.Extract); registerDacFxCommand(Constants.cmdImportBacpac, DacFxOperationType.Import); diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index 07efef28ef..156ea35292 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -852,7 +852,7 @@ export class LocConstants { selectSource: l10n.t("Select Source"), selectTarget: l10n.t("Select Target"), close: l10n.t("Close"), - dacFxApplicationFile: l10n.t("Data-tier Application File (.dacpac)"), + dacpacDialogFile: l10n.t("Data-tier Application File (.dacpac)"), databaseProject: l10n.t("Database Project"), ok: l10n.t("OK"), cancel: l10n.t("Cancel"), @@ -1087,7 +1087,7 @@ export class LocConstants { }; } - public get dacFxApplication() { + public get dacpacDialog() { return { title: l10n.t("Data-tier Application"), subtitle: l10n.t( diff --git a/src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx b/src/reactviews/pages/DacpacDialog/ApplicationInfoSection.tsx similarity index 81% rename from src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx rename to src/reactviews/pages/DacpacDialog/ApplicationInfoSection.tsx index 2c604da053..2e8ef38c21 100644 --- a/src/reactviews/pages/DacFxApplication/ApplicationInfoSection.tsx +++ b/src/reactviews/pages/DacpacDialog/ApplicationInfoSection.tsx @@ -38,23 +38,23 @@ export const ApplicationInfoSection = ({ return (
- + setApplicationName(data.value)} - placeholder={locConstants.dacFxApplication.enterApplicationName} + placeholder={locConstants.dacpacDialog.enterApplicationName} disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.applicationNameLabel} + aria-label={locConstants.dacpacDialog.applicationNameLabel} /> - + setApplicationVersion(data.value)} placeholder={DEFAULT_APPLICATION_VERSION} disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.applicationVersionLabel} + aria-label={locConstants.dacpacDialog.applicationVersionLabel} />
diff --git a/src/reactviews/pages/DacFxApplication/FilePathSection.tsx b/src/reactviews/pages/DacpacDialog/FilePathSection.tsx similarity index 82% rename from src/reactviews/pages/DacFxApplication/FilePathSection.tsx rename to src/reactviews/pages/DacpacDialog/FilePathSection.tsx index 770cb2bc5b..1066062864 100644 --- a/src/reactviews/pages/DacFxApplication/FilePathSection.tsx +++ b/src/reactviews/pages/DacpacDialog/FilePathSection.tsx @@ -56,8 +56,8 @@ export const FilePathSection = ({ onFilePathChange(data.value)} placeholder={ requiresInputFile - ? locConstants.dacFxApplication.selectPackageFile - : locConstants.dacFxApplication.selectOutputFile + ? locConstants.dacpacDialog.selectPackageFile + : locConstants.dacpacDialog.selectOutputFile } disabled={isOperationInProgress} aria-label={ requiresInputFile - ? locConstants.dacFxApplication.packageFileLabel - : locConstants.dacFxApplication.outputFileLabel + ? locConstants.dacpacDialog.packageFileLabel + : locConstants.dacpacDialog.outputFileLabel } /> diff --git a/src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx b/src/reactviews/pages/DacpacDialog/OperationTypeSection.tsx similarity index 69% rename from src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx rename to src/reactviews/pages/DacpacDialog/OperationTypeSection.tsx index ef76c21fda..dbdc6b300c 100644 --- a/src/reactviews/pages/DacFxApplication/OperationTypeSection.tsx +++ b/src/reactviews/pages/DacpacDialog/OperationTypeSection.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Field, makeStyles, Radio, RadioGroup } from "@fluentui/react-components"; -import { DacFxOperationType } from "../../../sharedInterfaces/dacFxApplication"; +import { DacFxOperationType } from "../../../sharedInterfaces/dacpacDialog"; import { locConstants } from "../../common/locConstants"; interface OperationTypeSectionProps { @@ -32,7 +32,7 @@ export const OperationTypeSection = ({ return (
- + { @@ -40,46 +40,46 @@ export const OperationTypeSection = ({ onOperationTypeChange?.(); }} disabled={isOperationInProgress} - aria-label={locConstants.dacFxApplication.operationLabel}> + aria-label={locConstants.dacpacDialog.operationLabel}> diff --git a/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx b/src/reactviews/pages/DacpacDialog/ServerSelectionSection.tsx similarity index 90% rename from src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx rename to src/reactviews/pages/DacpacDialog/ServerSelectionSection.tsx index 93621bd622..bab789eb63 100644 --- a/src/reactviews/pages/DacFxApplication/ServerSelectionSection.tsx +++ b/src/reactviews/pages/DacpacDialog/ServerSelectionSection.tsx @@ -45,17 +45,17 @@ export const ServerSelectionSection = ({ return (
{isConnecting ? ( - + ) : ( { @@ -71,10 +71,10 @@ export const ServerSelectionSection = ({ onServerChange(data.optionValue as string); }} disabled={isOperationInProgress || availableConnections.length === 0} - aria-label={locConstants.dacFxApplication.serverLabel}> + aria-label={locConstants.dacpacDialog.serverLabel}> {availableConnections.length === 0 ? ( ) : ( availableConnections.map((conn) => ( diff --git a/src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx b/src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx similarity index 86% rename from src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx rename to src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx index 2998f396ff..64ba8da6c4 100644 --- a/src/reactviews/pages/DacFxApplication/SourceDatabaseSection.tsx +++ b/src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx @@ -49,7 +49,7 @@ export const SourceDatabaseSection = ({
{showDatabaseSource ? ( setDatabaseName(data.optionText || "")} disabled={isOperationInProgress || !ownerUri} - aria-label={locConstants.dacFxApplication.sourceDatabaseLabel}> + aria-label={locConstants.dacpacDialog.sourceDatabaseLabel}> {availableDatabases.map((db) => (