Adding advanced connection settings pane to connection dialog (#17990)
* moving form components into tabs in react state * Semicolons * refactored out connection form * connection string dialog also refactored out * Moving files * refactoring out the idea of hiding components that are on a different tab * Consolidating form field creation * creating components from STS response * adding tooltips * ts-ignore error from typecheck * removing test reducer * adding advanced options drawer * Assigning advanced props to saved profile * removing unused import; PR comments * swapping console.logger * controller localization * cleaning up naming * cleanup * fixing string * bumping STS to consume string fixes
This commit is contained in:
Родитель
1aad8cdc0a
Коммит
3dc342e71a
|
@ -677,6 +677,57 @@
|
|||
<trans-unit id="queryFailed">
|
||||
<source xml:lang="en">Query failed</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="connectionDialog">
|
||||
<source xml:lang="en">Connection Dialog</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="azureAccount">
|
||||
<source xml:lang="en">Azure Account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="azureAccountIsRequired">
|
||||
<source xml:lang="en">Azure Account is required</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="selectAnAccount">
|
||||
<source xml:lang="en">Select an account</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="savePassword">
|
||||
<source xml:lang="en">Save Password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="tenantId">
|
||||
<source xml:lang="en">Tenant ID</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="selectATenant">
|
||||
<source xml:lang="en">Select a tenant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="tenantIdIsRequired">
|
||||
<source xml:lang="en">Tenant ID is required</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="profileName">
|
||||
<source xml:lang="en">Profile Name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="serverIsRequired">
|
||||
<source xml:lang="en">Server is required</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="usernameIsRequired">
|
||||
<source xml:lang="en">User name is required</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="connectionString">
|
||||
<source xml:lang="en">Connection String</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="connectionStringIsRequired">
|
||||
<source xml:lang="en">Connection string is required</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="signIn">
|
||||
<source xml:lang="en">Sign in</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="additionalParameters">
|
||||
<source xml:lang="en">Additional parameters</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="connect">
|
||||
<source xml:lang="en">Connect</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="parameters">
|
||||
<source xml:lang="en">Parameters</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
@ -68,11 +68,11 @@
|
|||
"@angular/upgrade": "~2.1.2",
|
||||
"@eslint/compat": "^1.1.0",
|
||||
"@eslint/js": "^9.5.0",
|
||||
"@fluentui/react-components": "^9.54.3",
|
||||
"@fluentui/react-components": "^9.54.13",
|
||||
"@jgoz/esbuild-plugin-typecheck": "^4.0.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@playwright/test": "^1.45.0",
|
||||
"@types/azdata": "^1.44.0",
|
||||
"@types/azdata": "^1.46.6",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/jquery": "^3.3.31",
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as LocalizedConstants from '../constants/localizedConstants';
|
||||
import * as utils from '../models/utils';
|
||||
import * as AzureConstants from './constants';
|
||||
import * as azureUtils from './utils';
|
||||
|
||||
|
@ -17,7 +16,7 @@ import providerSettings from '../azure/providerSettings';
|
|||
import VscodeWrapper from '../controllers/vscodeWrapper';
|
||||
import { ConnectionProfile } from '../models/connectionProfile';
|
||||
import { AzureAuthType, IAADResource, IAccount, IProviderSettings, ITenant, IToken } from '../models/contracts/azure';
|
||||
import { Logger, LogLevel } from '../models/logger';
|
||||
import { Logger } from '../models/logger';
|
||||
import { INameValueChoice, IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
|
||||
import { AccountStore } from './accountStore';
|
||||
import { ICredentialStore } from '../credentialstore/icredentialstore';
|
||||
|
@ -39,10 +38,9 @@ export abstract class AzureController {
|
|||
}
|
||||
|
||||
// Setup Logger
|
||||
let logLevel: LogLevel = LogLevel[utils.getConfigTracingLevel() as keyof typeof LogLevel];
|
||||
let pii = utils.getConfigPiiLogging();
|
||||
let _channel = this._vscodeWrapper.createOutputChannel(LocalizedConstants.azureLogChannelName);
|
||||
this.logger = new Logger(text => _channel.append(text), logLevel, pii);
|
||||
|
||||
let channel = this._vscodeWrapper.createOutputChannel(LocalizedConstants.azureLogChannelName);
|
||||
this.logger = Logger.create(channel);
|
||||
|
||||
this._providerSettings = providerSettings;
|
||||
vscode.workspace.onDidChangeConfiguration((changeEvent) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"service": {
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "5.0.20240724.1",
|
||||
"version": "5.0.20240823.2",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-net8.0.zip",
|
||||
"Windows_64": "win-x64-net8.0.zip",
|
||||
|
|
|
@ -5,16 +5,24 @@
|
|||
|
||||
import * as vscode from 'vscode';
|
||||
import { ReactWebViewPanelController } from "../controllers/reactWebviewController";
|
||||
import { ApiStatus, AuthenticationType, ConnectionDialogReducers, ConnectionDialogWebviewState, FormComponent, FormComponentActionButton, FormComponentOptions, FormComponentType, FormTabs, IConnectionDialogProfile } from '../sharedInterfaces/connectionDialog';
|
||||
import { ApiStatus, AuthenticationType, ConnectionDialogReducers, ConnectionDialogWebviewState, FormComponent, FormComponentActionButton, FormComponentOptions, FormComponentType, FormTabType, IConnectionDialogProfile } from '../sharedInterfaces/connectionDialog';
|
||||
import { IConnectionInfo } from 'vscode-mssql';
|
||||
import MainController from '../controllers/mainController';
|
||||
import { getConnectionDisplayName } from '../models/connectionInfo';
|
||||
import { AzureController } from '../azure/azureController';
|
||||
import { ObjectExplorerProvider } from '../objectExplorer/objectExplorerProvider';
|
||||
import { WebviewRoute } from '../sharedInterfaces/webviewRoutes';
|
||||
import { CapabilitiesResult, GetCapabilitiesRequest } from '../models/contracts/connection';
|
||||
import { ConnectionOption } from 'azdata';
|
||||
import { Logger } from '../models/logger';
|
||||
import VscodeWrapper from '../controllers/vscodeWrapper';
|
||||
import * as LocalizedConstants from '../constants/localizedConstants';
|
||||
|
||||
export class ConnectionDialogWebViewController extends ReactWebViewPanelController<ConnectionDialogWebviewState, ConnectionDialogReducers> {
|
||||
private _connectionToEditCopy: IConnectionDialogProfile | undefined;
|
||||
|
||||
private static _logger: Logger;
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
private _mainController: MainController,
|
||||
|
@ -23,13 +31,17 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
) {
|
||||
super(
|
||||
context,
|
||||
'Connection Dialog',
|
||||
LocalizedConstants.connectionDialog,
|
||||
WebviewRoute.connectionDialog,
|
||||
{
|
||||
recentConnections: [],
|
||||
selectedFormTab: FormTabs.Parameters,
|
||||
connectionProfile: {} as IConnectionDialogProfile,
|
||||
formComponents: [],
|
||||
recentConnections: [],
|
||||
selectedFormTab: FormTabType.Parameters,
|
||||
connectionFormComponents: {
|
||||
mainComponents: [],
|
||||
advancedComponents: {}
|
||||
},
|
||||
connectionStringComponents: [],
|
||||
connectionStatus: ApiStatus.NotStarted,
|
||||
formError: ''
|
||||
},
|
||||
|
@ -39,6 +51,13 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
light: vscode.Uri.joinPath(context.extensionUri, 'media', 'connectionDialogEditor.svg')
|
||||
}
|
||||
);
|
||||
|
||||
if (!ConnectionDialogWebViewController._logger) {
|
||||
const vscodeWrapper = new VscodeWrapper();
|
||||
const channel = vscodeWrapper.createOutputChannel(LocalizedConstants.connectionDialog);
|
||||
ConnectionDialogWebViewController._logger = Logger.create(channel);
|
||||
}
|
||||
|
||||
this.registerRpcHandlers();
|
||||
this.initializeDialog().catch(err => vscode.window.showErrorMessage(err.toString()));
|
||||
}
|
||||
|
@ -50,7 +69,11 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
} else {
|
||||
await this.loadEmptyConnection();
|
||||
}
|
||||
this.state.formComponents = await this.generateFormComponents();
|
||||
|
||||
|
||||
this.state.connectionFormComponents = await this.generateConnectionFormComponents();
|
||||
this.state.connectionStringComponents = await this.generateConnectionStringComponents();
|
||||
|
||||
await this.updateItemVisibility();
|
||||
this.state = this.state;
|
||||
}
|
||||
|
@ -109,23 +132,7 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
private async updateItemVisibility() {
|
||||
const selectedTab = this.state.selectedFormTab;
|
||||
let hiddenProperties: (keyof IConnectionDialogProfile)[] = [];
|
||||
if (selectedTab === FormTabs.ConnectionString) {
|
||||
hiddenProperties = [
|
||||
'server',
|
||||
'authenticationType',
|
||||
'user',
|
||||
'password',
|
||||
'savePassword',
|
||||
'accountId',
|
||||
'tenantId',
|
||||
'database',
|
||||
'trustServerCertificate',
|
||||
'encrypt'
|
||||
];
|
||||
} else {
|
||||
hiddenProperties = [
|
||||
'connectionString'
|
||||
];
|
||||
if (selectedTab === FormTabType.Parameters) {
|
||||
if (this.state.connectionProfile.authenticationType !== AuthenticationType.SqlLogin) {
|
||||
hiddenProperties.push('user', 'password', 'savePassword');
|
||||
}
|
||||
|
@ -138,22 +145,24 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
if (tenants.length === 1) {
|
||||
hiddenProperties.push('tenantId');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.state.formComponents.length; i++) {
|
||||
const component = this.state.formComponents[i];
|
||||
if (hiddenProperties.includes(component.propertyName)) {
|
||||
component.hidden = true;
|
||||
} else {
|
||||
component.hidden = false;
|
||||
}
|
||||
for (const component of this.state.connectionFormComponents.mainComponents) {
|
||||
component.hidden = hiddenProperties.includes(component.propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
private getActiveFormComponents(): FormComponent[] {
|
||||
if (this.state.selectedFormTab === FormTabType.Parameters) {
|
||||
return this.state.connectionFormComponents.mainComponents;
|
||||
}
|
||||
return this.state.connectionStringComponents;
|
||||
}
|
||||
|
||||
private getFormComponent(propertyName: keyof IConnectionDialogProfile): FormComponent | undefined {
|
||||
return this.state.formComponents.find(c => c.propertyName === propertyName);
|
||||
|
||||
return this.getActiveFormComponents().find(c => c.propertyName === propertyName);
|
||||
}
|
||||
|
||||
private async getAccounts(): Promise<FormComponentOptions[]> {
|
||||
|
@ -184,108 +193,87 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
});
|
||||
}
|
||||
|
||||
private async generateFormComponents(): Promise<FormComponent[]> {
|
||||
const result: FormComponent[] = [
|
||||
{
|
||||
type: FormComponentType.Input,
|
||||
propertyName: 'server',
|
||||
label: 'Server',
|
||||
required: true,
|
||||
validate: (value: string) => {
|
||||
if (this.state.selectedFormTab === FormTabs.Parameters && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: 'Server is required'
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
type: FormComponentType.TextArea,
|
||||
propertyName: 'connectionString',
|
||||
label: 'Connection String',
|
||||
required: true,
|
||||
validate: (value: string) => {
|
||||
if (this.state.selectedFormTab === FormTabs.ConnectionString && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: 'Connection string is required'
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
type: FormComponentType.Dropdown,
|
||||
propertyName: 'authenticationType',
|
||||
label: 'Authentication Type',
|
||||
required: true,
|
||||
options: [
|
||||
{
|
||||
displayName: 'SQL Login',
|
||||
value: AuthenticationType.SqlLogin
|
||||
},
|
||||
{
|
||||
displayName: 'Windows Authentication',
|
||||
value: AuthenticationType.Integrated
|
||||
},
|
||||
{
|
||||
displayName: 'Microsoft Entra MFA',
|
||||
value: AuthenticationType.AzureMFA
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
// Hidden if connection string is set or if the authentication type is not SQL Login
|
||||
propertyName: 'user',
|
||||
label: 'User Name',
|
||||
type: FormComponentType.Input,
|
||||
required: true,
|
||||
validate: (value: string) => {
|
||||
if (this.state.connectionProfile.authenticationType === AuthenticationType.SqlLogin && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: 'User name is required'
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
propertyName: 'password',
|
||||
label: 'Password',
|
||||
required: false,
|
||||
private convertToFormComponent(connOption: ConnectionOption): FormComponent {
|
||||
switch (connOption.valueType) {
|
||||
case 'boolean':
|
||||
return {
|
||||
propertyName: connOption.name as keyof IConnectionDialogProfile,
|
||||
label: connOption.displayName,
|
||||
required: connOption.isRequired,
|
||||
type: FormComponentType.Checkbox,
|
||||
tooltip: connOption.description,
|
||||
};
|
||||
case 'string':
|
||||
return {
|
||||
propertyName: connOption.name as keyof IConnectionDialogProfile,
|
||||
label: connOption.displayName,
|
||||
required: connOption.isRequired,
|
||||
type: FormComponentType.Input,
|
||||
tooltip: connOption.description,
|
||||
};
|
||||
case 'password':
|
||||
return {
|
||||
propertyName: connOption.name as keyof IConnectionDialogProfile,
|
||||
label: connOption.displayName,
|
||||
required: connOption.isRequired,
|
||||
type: FormComponentType.Password,
|
||||
},
|
||||
{
|
||||
tooltip: connOption.description,
|
||||
};
|
||||
|
||||
case 'number':
|
||||
return {
|
||||
propertyName: connOption.name as keyof IConnectionDialogProfile,
|
||||
label: connOption.displayName,
|
||||
required: connOption.isRequired,
|
||||
type: FormComponentType.Input,
|
||||
tooltip: connOption.description,
|
||||
};
|
||||
case 'category':
|
||||
return {
|
||||
propertyName: connOption.name as keyof IConnectionDialogProfile,
|
||||
label: connOption.displayName,
|
||||
required: connOption.isRequired,
|
||||
type: FormComponentType.Dropdown,
|
||||
tooltip: connOption.description,
|
||||
options: connOption.categoryValues.map(v => {
|
||||
return {
|
||||
displayName: v.displayName ?? v.name, // Use name if displayName is not provided
|
||||
value: v.name
|
||||
};
|
||||
}),
|
||||
};
|
||||
default:
|
||||
ConnectionDialogWebViewController._logger.log(`Unhandled connection option type: ${connOption.valueType}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async completeFormComponents(components: Map<string, {option: ConnectionOption, component: FormComponent}>) {
|
||||
// Add additional components that are not part of the connection options
|
||||
components.set('savePassword', {
|
||||
option: undefined,
|
||||
component: {
|
||||
propertyName: 'savePassword',
|
||||
label: 'Save Password',
|
||||
label: LocalizedConstants.savePassword,
|
||||
required: false,
|
||||
type: FormComponentType.Checkbox,
|
||||
},
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
components.set('accountId', {
|
||||
option: undefined,
|
||||
component: {
|
||||
propertyName: 'accountId',
|
||||
label: 'Azure Account',
|
||||
label: LocalizedConstants.azureAccount,
|
||||
required: true,
|
||||
type: FormComponentType.Dropdown,
|
||||
options: await this.getAccounts(),
|
||||
placeholder: 'Select an account',
|
||||
placeholder: LocalizedConstants.selectAnAccount,
|
||||
actionButtons: await this.getAzureActionButtons(),
|
||||
validate: (value: string) => {
|
||||
if (this.state.connectionProfile.authenticationType === AuthenticationType.AzureMFA && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: 'Azure Account is required'
|
||||
validationMessage: LocalizedConstants.azureAccountIsRequired
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
@ -293,20 +281,135 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
validationMessage: ''
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
components.set('tenantId', {
|
||||
option: undefined,
|
||||
component: {
|
||||
propertyName: 'tenantId',
|
||||
label: 'Tenant ID',
|
||||
label: LocalizedConstants.tenantId,
|
||||
required: true,
|
||||
type: FormComponentType.Dropdown,
|
||||
options: [],
|
||||
hidden: true,
|
||||
placeholder: 'Select a tenant',
|
||||
placeholder: LocalizedConstants.selectATenant,
|
||||
validate: (value: string) => {
|
||||
if (this.state.connectionProfile.authenticationType === AuthenticationType.AzureMFA && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: 'Tenant ID is required'
|
||||
validationMessage: LocalizedConstants.tenantIdIsRequired
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
components.set('profileName', {
|
||||
option: undefined,
|
||||
component: {
|
||||
propertyName: 'profileName',
|
||||
label: LocalizedConstants.profileName,
|
||||
required: false,
|
||||
type: FormComponentType.Input,
|
||||
}
|
||||
});
|
||||
|
||||
// add missing validation functions for generated components
|
||||
components.get('server')!.component.validate = (value: string) => {
|
||||
if (this.state.selectedFormTab === FormTabType.Parameters && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: LocalizedConstants.serverIsRequired
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
};
|
||||
};
|
||||
|
||||
components.get('user')!.component.validate = (value: string) => {
|
||||
if (this.state.connectionProfile.authenticationType === AuthenticationType.SqlLogin && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: LocalizedConstants.usernameIsRequired
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
validationMessage: ''
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private _mainOptionNames = new Set<string>([
|
||||
'server',
|
||||
'authenticationType',
|
||||
'user',
|
||||
'password',
|
||||
'savePassword',
|
||||
'accountId',
|
||||
'tenantId',
|
||||
'database',
|
||||
'trustServerCertificate',
|
||||
'encrypt',
|
||||
'profileName'
|
||||
]);
|
||||
|
||||
private async generateConnectionFormComponents(): Promise<{
|
||||
mainComponents: FormComponent[],
|
||||
advancedComponents: {[category: string]: FormComponent[]}
|
||||
}> {
|
||||
// get list of connection options from Tools Service
|
||||
const result: CapabilitiesResult = await this._mainController.connectionManager.client.sendRequest(GetCapabilitiesRequest.type, {});
|
||||
const connectionOptions: ConnectionOption[] = result.capabilities.connectionProvider.options;
|
||||
|
||||
// convert connection options to form components
|
||||
const allConnectionFormComponents = new Map<string, {option: ConnectionOption, component: FormComponent}>();
|
||||
|
||||
for (const option of connectionOptions) {
|
||||
allConnectionFormComponents.set(option.name, {option, component: this.convertToFormComponent(option)});
|
||||
}
|
||||
|
||||
await this.completeFormComponents(allConnectionFormComponents);
|
||||
|
||||
// organize the main components and advanced components
|
||||
// main components are few-enough that there's no grouping, but advanced components get grouped by category
|
||||
const mainComponents: FormComponent[] = [];
|
||||
const advancedComponents: {[category: string]: FormComponent[]} = {};
|
||||
|
||||
for (const [optionName, {option, component}] of allConnectionFormComponents) {
|
||||
if (this._mainOptionNames.has(optionName)) {
|
||||
mainComponents.push(component);
|
||||
} else {
|
||||
if (!advancedComponents[option.groupName]) {
|
||||
advancedComponents[option.groupName] = [component];
|
||||
} else {
|
||||
advancedComponents[option.groupName].push(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {mainComponents, advancedComponents};
|
||||
}
|
||||
|
||||
private async generateConnectionStringComponents(): Promise<FormComponent[]> {
|
||||
return [
|
||||
{
|
||||
type: FormComponentType.TextArea,
|
||||
propertyName: 'connectionString',
|
||||
label: LocalizedConstants.connectionString,
|
||||
required: true,
|
||||
validate: (value: string) => {
|
||||
if (this.state.selectedFormTab === FormTabType.ConnectionString && !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
validationMessage: LocalizedConstants.connectionStringIsRequired
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
@ -315,46 +418,13 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
propertyName: 'database',
|
||||
label: 'Database',
|
||||
required: false,
|
||||
type: FormComponentType.Input,
|
||||
},
|
||||
{
|
||||
propertyName: 'trustServerCertificate',
|
||||
label: 'Trust Server Certificate',
|
||||
required: false,
|
||||
type: FormComponentType.Checkbox,
|
||||
},
|
||||
{
|
||||
propertyName: 'encrypt',
|
||||
label: 'Encrypt Connection',
|
||||
required: false,
|
||||
type: FormComponentType.Dropdown,
|
||||
options: [
|
||||
{
|
||||
displayName: 'Optional',
|
||||
value: 'Optional'
|
||||
},
|
||||
{
|
||||
displayName: 'Mandatory',
|
||||
value: 'Mandatory'
|
||||
},
|
||||
{
|
||||
displayName: 'Strict (Requires SQL Server 2022 or Azure SQL)',
|
||||
value: 'Strict'
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
propertyName: 'profileName',
|
||||
label: 'Profile Name',
|
||||
label: LocalizedConstants.profileName,
|
||||
required: false,
|
||||
type: FormComponentType.Input,
|
||||
}
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
private async validateFormComponents(propertyName?: keyof IConnectionDialogProfile): Promise<number> {
|
||||
|
@ -369,7 +439,7 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
}
|
||||
}
|
||||
else {
|
||||
this.state.formComponents.forEach(c => {
|
||||
this.getActiveFormComponents().forEach(c => {
|
||||
if (c.hidden) {
|
||||
c.validation = {
|
||||
isValid: true,
|
||||
|
@ -392,7 +462,7 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
private async getAzureActionButtons(): Promise<FormComponentActionButton[]> {
|
||||
const actionButtons: FormComponentActionButton[] = [];
|
||||
actionButtons.push({
|
||||
label: 'Sign in',
|
||||
label: LocalizedConstants.signIn,
|
||||
id: 'azureSignIn',
|
||||
callback: async () => {
|
||||
const account = await this._mainController.azureAccountService.addAccount();
|
||||
|
@ -412,13 +482,13 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
const isTokenExpired = AzureController.isTokenInValid(session.token, session.expiresOn);
|
||||
if (isTokenExpired) {
|
||||
actionButtons.push({
|
||||
label: 'Refresh Token',
|
||||
label: LocalizedConstants.refreshTokenLabel,
|
||||
id: 'refreshToken',
|
||||
callback: async () => {
|
||||
const account = (await this._mainController.azureAccountService.getAccounts()).find(account => account.displayInfo.userId === this.state.connectionProfile.accountId);
|
||||
if (account) {
|
||||
const session = await this._mainController.azureAccountService.getAccountSecurityToken(account, undefined);
|
||||
console.log('Token refreshed', session.expiresOn);
|
||||
ConnectionDialogWebViewController._logger.log('Token refreshed', session.expiresOn);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -470,8 +540,8 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
|
||||
private clearFormError() {
|
||||
this.state.formError = '';
|
||||
for (let i = 0; i < this.state.formComponents.length; i++) {
|
||||
this.state.formComponents[i].validation = undefined;
|
||||
for (const component of this.getActiveFormComponents()) {
|
||||
component.validation = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -515,13 +585,26 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
this.state.connectionStatus = ApiStatus.Loading;
|
||||
this.state.formError = '';
|
||||
this.state = this.state;
|
||||
const notHiddenComponents = this.state.formComponents.filter(c => !c.hidden).map(c => c.propertyName);
|
||||
// Set all other fields to undefined
|
||||
Object.keys(this.state.connectionProfile).forEach(key => {
|
||||
if (!notHiddenComponents.includes(key as keyof IConnectionDialogProfile)) {
|
||||
(this.state.connectionProfile[key as keyof IConnectionDialogProfile] as any) = undefined;
|
||||
|
||||
const usedFields = new Set<keyof IConnectionDialogProfile>(this.getActiveFormComponents().filter(c => !c.hidden).map(c => c.propertyName));
|
||||
|
||||
Object.keys(this.state.connectionFormComponents.advancedComponents).forEach(group => {
|
||||
this.state.connectionFormComponents.advancedComponents[group].forEach(c => {
|
||||
if (!c.hidden) {
|
||||
usedFields.add(c.propertyName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clear unused fields (anything that isn't visible due to form selections and isn't an advanced option)
|
||||
Object.keys(this.state.connectionProfile).forEach(optionName => {
|
||||
if (!usedFields.has(optionName as keyof IConnectionDialogProfile)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this.state.connectionProfile[optionName as keyof IConnectionDialogProfile] as any) = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Perform final validation of all inputs
|
||||
const errorCount = await this.validateFormComponents();
|
||||
if (errorCount > 0) {
|
||||
this.state.connectionStatus = ApiStatus.Error;
|
||||
|
@ -555,4 +638,4 @@ export class ConnectionDialogWebViewController extends ReactWebViewPanelControll
|
|||
return state;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
import * as path from 'path';
|
||||
import VscodeWrapper from '../controllers/vscodeWrapper';
|
||||
import * as Utils from '../models/utils';
|
||||
import { Logger, LogLevel } from '../models/logger';
|
||||
import { Logger } from '../models/logger';
|
||||
import * as Constants from '../constants/constants';
|
||||
import ServerProvider from './server';
|
||||
import ServiceDownloadProvider from './serviceDownloadProvider';
|
||||
|
@ -156,10 +156,10 @@ export default class SqlToolsServiceClient {
|
|||
if (SqlToolsServiceClient._instance === undefined) {
|
||||
let config = new ExtConfig();
|
||||
let vscodeWrapper = new VscodeWrapper();
|
||||
let logLevel: LogLevel = LogLevel[Utils.getConfigTracingLevel() as keyof typeof LogLevel];
|
||||
let pii = Utils.getConfigPiiLogging();
|
||||
|
||||
_channel = vscodeWrapper.createOutputChannel(Constants.serviceInitializingOutputChannelName);
|
||||
let logger = new Logger(text => _channel.append(text), logLevel, pii);
|
||||
let logger = Logger.create(_channel);
|
||||
|
||||
let serverStatusView = new ServerStatusView();
|
||||
let httpClient = new HttpClient();
|
||||
let decompressProvider = new DecompressProvider();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DataProtocolServerCapabilities } from 'azdata';
|
||||
import { NotificationType, RequestType } from 'vscode-languageclient';
|
||||
import { ConnectionDetails, IServerInfo } from 'vscode-mssql';
|
||||
|
||||
|
@ -258,3 +259,18 @@ export namespace EncryptionKeysChangedNotification {
|
|||
export namespace ClearPooledConnectionsRequest {
|
||||
export const type = new RequestType<object, void, void, void>('connection/clearpooledconnections');
|
||||
}
|
||||
|
||||
//#region Connection capabilities
|
||||
|
||||
/**
|
||||
* Gets the capabilities of the data protocol server
|
||||
*/
|
||||
export namespace GetCapabilitiesRequest {
|
||||
export const type = new RequestType<object, CapabilitiesResult, void, void>('capabilities/list');
|
||||
}
|
||||
|
||||
export interface CapabilitiesResult {
|
||||
capabilities: DataProtocolServerCapabilities;
|
||||
}
|
||||
|
||||
//#endregion
|
|
@ -6,6 +6,7 @@
|
|||
import * as os from 'os';
|
||||
import { ILogger } from './interfaces';
|
||||
import * as Utils from './utils';
|
||||
import { OutputChannel } from 'vscode';
|
||||
|
||||
export enum LogLevel {
|
||||
'Pii',
|
||||
|
@ -38,6 +39,13 @@ export class Logger implements ILogger {
|
|||
this._prefix = prefix;
|
||||
}
|
||||
|
||||
public static create(channel: OutputChannel){
|
||||
const logLevel: LogLevel = LogLevel[Utils.getConfigTracingLevel() as keyof typeof LogLevel];
|
||||
const pii = Utils.getConfigPiiLogging();
|
||||
|
||||
return new Logger(text => channel.append(text), logLevel, pii);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message containing PII (when enabled). Provides the ability to sanitize or shorten values to hide information or reduce the amount logged.
|
||||
* @param msg The initial message to log
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Input, Button, Textarea, Dropdown, Checkbox, Option, makeStyles, Field, InfoLabel, LabelProps } from "@fluentui/react-components";
|
||||
import { EyeRegular, EyeOffRegular } from "@fluentui/react-icons";
|
||||
|
||||
import { ConnectionDialogContextProps, FormComponent, FormComponentType, IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
|
||||
import { ConnectionDialogContext } from "../../pages/ConnectionDialog/connectionDialogStateProvider";
|
||||
|
||||
export const FormInput = ({ value, target, type }: { value: string, target: keyof IConnectionDialogProfile, type: 'input' | 'password' | 'textarea' }) => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
const [formInputValue, setFormInputValue] = useState(value);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFormInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (data: string) => {
|
||||
setFormInputValue(data);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
connectionDialogContext?.formAction({
|
||||
propertyName: target,
|
||||
isAction: false,
|
||||
value: formInputValue
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
type === 'input' &&
|
||||
<Input
|
||||
value={formInputValue}
|
||||
onChange={(_value, data) => handleChange(data.value)}
|
||||
onBlur={handleBlur}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
{
|
||||
type === 'password' &&
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formInputValue}
|
||||
onChange={(_value, data) => handleChange(data.value)}
|
||||
onBlur={handleBlur}
|
||||
size="small"
|
||||
contentAfter={
|
||||
<Button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
icon={showPassword ? <EyeRegular /> : <EyeOffRegular />}
|
||||
appearance="transparent"
|
||||
size="small"
|
||||
>
|
||||
</Button>}
|
||||
/>
|
||||
}
|
||||
{
|
||||
type === 'textarea' &&
|
||||
<Textarea
|
||||
value={formInputValue}
|
||||
size="small"
|
||||
onChange={(_value, data) => handleChange(data.value)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormField = ({connectionDialogContext, component, idx}: { connectionDialogContext: ConnectionDialogContextProps, component: FormComponent, idx: number}) => {
|
||||
const formStyles = useFormStyles();
|
||||
|
||||
return (
|
||||
<div className={formStyles.formComponentDiv} key={idx}>
|
||||
<Field
|
||||
validationMessage={component.validation?.validationMessage ?? ''}
|
||||
orientation={component.type === FormComponentType.Checkbox ? 'horizontal' : 'vertical'}
|
||||
validationState={component.validation ? (component.validation.isValid ? 'none' : 'error') : 'none'}
|
||||
required={component.required}
|
||||
// @ts-ignore there's a bug in the typings somewhere, so ignoring this line to avoid angering type-checker
|
||||
label={
|
||||
component.tooltip
|
||||
? {
|
||||
children: (_: unknown, slotProps: LabelProps) => (
|
||||
<InfoLabel {...slotProps} info={component.tooltip}>
|
||||
{ component.label }
|
||||
</InfoLabel>
|
||||
)
|
||||
}
|
||||
: component.label}
|
||||
>
|
||||
{ generateFormComponent(connectionDialogContext, component, idx) }
|
||||
</Field>
|
||||
{
|
||||
component?.actionButtons?.length! > 0 &&
|
||||
<div className={formStyles.formComponentActionDiv}>
|
||||
{
|
||||
component.actionButtons?.map((actionButton, idx) => {
|
||||
return <Button shape="square" key={idx + actionButton.id} appearance='outline' style={
|
||||
{
|
||||
width: '120px'
|
||||
}
|
||||
} onClick={() => connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: true,
|
||||
value: actionButton.id
|
||||
})}>{actionButton.label}</Button>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function generateFormField(connectionDialogContext: ConnectionDialogContextProps, component: FormComponent, idx: number, formStyles: Record<"formRoot" | "formDiv" | "formComponentDiv" | "formComponentActionDiv", string>) {
|
||||
return (
|
||||
<div className={formStyles.formComponentDiv} key={idx}>
|
||||
<Field
|
||||
validationMessage={component.validation?.validationMessage ?? ''}
|
||||
orientation={component.type === FormComponentType.Checkbox ? 'horizontal' : 'vertical'}
|
||||
validationState={component.validation ? (component.validation.isValid ? 'none' : 'error') : 'none'}
|
||||
required={component.required}
|
||||
label={component.label}>
|
||||
{ generateFormComponent(connectionDialogContext, component, idx) }
|
||||
</Field>
|
||||
{
|
||||
component?.actionButtons?.length! > 0 &&
|
||||
<div className={formStyles.formComponentActionDiv}>
|
||||
{
|
||||
component.actionButtons?.map((actionButton, idx) => {
|
||||
return <Button shape="square" key={idx + actionButton.id} appearance='outline' style={
|
||||
{
|
||||
width: '120px'
|
||||
}
|
||||
} onClick={() => connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: true,
|
||||
value: actionButton.id
|
||||
})}>{actionButton.label}</Button>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateFormComponent(connectionDialogContext: ConnectionDialogContextProps, component: FormComponent, _idx: number) {
|
||||
const profile = connectionDialogContext.state.connectionProfile;
|
||||
|
||||
switch (component.type) {
|
||||
case FormComponentType.Input:
|
||||
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='input' />;
|
||||
case FormComponentType.TextArea:
|
||||
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='textarea' />;
|
||||
case FormComponentType.Password:
|
||||
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='password' />;
|
||||
case FormComponentType.Dropdown:
|
||||
if (component.options === undefined) {
|
||||
throw new Error('Dropdown component must have options');
|
||||
}
|
||||
return <Dropdown
|
||||
size="small"
|
||||
placeholder={component.placeholder ?? ''}
|
||||
value={component.options.find(option => option.value === profile[component.propertyName])?.displayName ?? ''}
|
||||
selectedOptions={[profile[component.propertyName] as string]}
|
||||
onOptionSelect={(_event, data) => {
|
||||
connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: false,
|
||||
value: data.optionValue as string
|
||||
});
|
||||
}}>
|
||||
{
|
||||
component.options?.map((option, idx) => {
|
||||
return <Option key={component.propertyName + idx} value={option.value}>{option.displayName}</Option>;
|
||||
})
|
||||
}
|
||||
</Dropdown>;
|
||||
case FormComponentType.Checkbox:
|
||||
return <Checkbox
|
||||
size="medium"
|
||||
checked={profile[component.propertyName] as boolean ?? false}
|
||||
onChange={(_value, data) => connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: false,
|
||||
value: data.checked
|
||||
})}
|
||||
/>;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const useFormStyles = makeStyles({
|
||||
formRoot: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
formDiv: {
|
||||
padding: '10px',
|
||||
maxWidth: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
},
|
||||
formComponentDiv: {
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
},
|
||||
formComponentActionDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { createContext } from "react";
|
||||
import { useVscodeWebview } from "../../common/vscodeWebViewProvider";
|
||||
import { ConnectionDialogContextProps, ConnectionDialogReducers, ConnectionDialogWebviewState, FormTabs, IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
|
||||
import { ConnectionDialogContextProps, ConnectionDialogReducers, ConnectionDialogWebviewState, FormTabType, IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
|
||||
|
||||
const ConnectionDialogContext = createContext<ConnectionDialogContextProps | undefined>(undefined);
|
||||
|
||||
|
@ -23,21 +23,21 @@ const ConnectionDialogStateProvider: React.FC<ConnectionDialogProviderProps> = (
|
|||
loadConnection: function (connection: IConnectionDialogProfile): void {
|
||||
webViewState?.extensionRpc.action('loadConnection', {
|
||||
connection: connection,
|
||||
});
|
||||
});
|
||||
},
|
||||
formAction: function (event): void {
|
||||
webViewState?.extensionRpc.action('formAction', {
|
||||
event: event
|
||||
});
|
||||
},
|
||||
setFormTab: function (tab: FormTabs): void {
|
||||
setFormTab: function (tab: FormTabType): void {
|
||||
webViewState?.extensionRpc.action('setFormTab', {
|
||||
tab: tab
|
||||
});
|
||||
},
|
||||
connect: function (): void {
|
||||
webViewState?.extensionRpc.action('connect');
|
||||
}
|
||||
},
|
||||
}
|
||||
}>{children}</ConnectionDialogContext.Provider>;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useContext, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
DrawerBody,
|
||||
DrawerHeader,
|
||||
DrawerHeaderTitle,
|
||||
MessageBar,
|
||||
OverlayDrawer,
|
||||
Spinner,
|
||||
} from "@fluentui/react-components";
|
||||
import { Dismiss24Regular } from "@fluentui/react-icons";
|
||||
|
||||
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
|
||||
import { ApiStatus } from "../../../sharedInterfaces/connectionDialog";
|
||||
import { FormField, useFormStyles } from "../../common/forms/formUtils";
|
||||
|
||||
export const ConnectionFormPage = () => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
const formStyles = useFormStyles();
|
||||
const [isAdvancedDrawerOpen, setIsAdvancedDrawerOpen] = useState(false);
|
||||
|
||||
if (connectionDialogContext === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={formStyles.formDiv}>
|
||||
{connectionDialogContext?.state.formError && (
|
||||
<MessageBar intent="error">
|
||||
{connectionDialogContext.state.formError}
|
||||
</MessageBar>
|
||||
)}
|
||||
{connectionDialogContext.state.connectionFormComponents.mainComponents.map(
|
||||
(component, idx) => {
|
||||
if (component.hidden === true) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<FormField
|
||||
key={idx}
|
||||
connectionDialogContext={connectionDialogContext}
|
||||
component={component}
|
||||
idx={idx}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<OverlayDrawer
|
||||
position="end"
|
||||
size="medium"
|
||||
open={isAdvancedDrawerOpen}
|
||||
onOpenChange={(_, { open }) => setIsAdvancedDrawerOpen(open)}
|
||||
>
|
||||
<DrawerHeader>
|
||||
<DrawerHeaderTitle
|
||||
action={
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Close"
|
||||
icon={<Dismiss24Regular />}
|
||||
onClick={() => setIsAdvancedDrawerOpen(false)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Advanced Connection Settings
|
||||
</DrawerHeaderTitle>
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
{Object.keys(
|
||||
connectionDialogContext.state.connectionFormComponents
|
||||
.advancedComponents
|
||||
).map((group, groupIndex) => {
|
||||
return (
|
||||
<div key={groupIndex} style={{ margin: "20px 0px" }}>
|
||||
<Divider>{group}</Divider>
|
||||
{connectionDialogContext.state.connectionFormComponents.advancedComponents[
|
||||
group
|
||||
].map((component, idx) => {
|
||||
if (component.hidden === true) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<FormField
|
||||
key={idx}
|
||||
connectionDialogContext={connectionDialogContext}
|
||||
component={component}
|
||||
idx={idx}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DrawerBody>
|
||||
</OverlayDrawer>
|
||||
<Button
|
||||
shape="square"
|
||||
onClick={(_event) => {
|
||||
setIsAdvancedDrawerOpen(!isAdvancedDrawerOpen);
|
||||
}}
|
||||
style={{
|
||||
width: "200px",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={
|
||||
connectionDialogContext.state.connectionStatus === ApiStatus.Loading
|
||||
}
|
||||
shape="square"
|
||||
onClick={(_event) => {
|
||||
connectionDialogContext.connect();
|
||||
}}
|
||||
style={{
|
||||
width: "200px",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
iconPosition="after"
|
||||
icon={
|
||||
connectionDialogContext.state.connectionStatus ===
|
||||
ApiStatus.Loading ? (
|
||||
<Spinner size="tiny" />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useContext } from "react";
|
||||
import { Text, Image, webLightTheme } from "@fluentui/react-components";
|
||||
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
|
||||
|
||||
const sqlServerImage = require('../../../../media/sqlServer.svg');
|
||||
const sqlServerImageDark = require('../../../../media/sqlServer_inverse.svg');
|
||||
|
||||
export const ConnectionHeader = () => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
|
||||
return (
|
||||
<div style={
|
||||
{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}
|
||||
}>
|
||||
<Image style={
|
||||
{
|
||||
padding: '10px',
|
||||
}
|
||||
}
|
||||
src={connectionDialogContext?.theme === webLightTheme ? sqlServerImage : sqlServerImageDark} alt='SQL Server' height={60} width={60} />
|
||||
<Text size={500} style={
|
||||
{
|
||||
lineHeight: '60px'
|
||||
}
|
||||
} weight='medium'>Connect to SQL Server</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,254 +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 { useContext, useEffect, useState } from "react";
|
||||
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
|
||||
import { Text, Button, Checkbox, Dropdown, Field, Input, Option, Tab, TabList, makeStyles, Image, MessageBar, Textarea, webLightTheme, Spinner } from "@fluentui/react-components";
|
||||
import { ApiStatus, FormComponent, FormComponentType, FormTabs, IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
|
||||
import { EyeRegular, EyeOffRegular } from "@fluentui/react-icons";
|
||||
import './sqlServerRotation.css';
|
||||
const sqlServerImage = require('../../../../media/sqlServer.svg');
|
||||
const sqlServerImageDark = require('../../../../media/sqlServer_inverse.svg');
|
||||
|
||||
const useStyles = makeStyles({
|
||||
formRoot: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
formDiv: {
|
||||
padding: '10px',
|
||||
maxWidth: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
},
|
||||
formComponentDiv: {
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
},
|
||||
formComponentActionDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const FormInput = ({ value, target, type }: { value: string, target: keyof IConnectionDialogProfile, type: 'input' | 'password' | 'textarea' }) => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
const [inputVal, setValueVal] = useState(value);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setValueVal(value);
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (data: string) => {
|
||||
setValueVal(data);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
connectionDialogContext?.formAction({
|
||||
propertyName: target,
|
||||
isAction: false,
|
||||
value: inputVal
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
type === 'input' &&
|
||||
<Input
|
||||
value={inputVal}
|
||||
onChange={(_value, data) => handleChange(data.value)}
|
||||
onBlur={handleBlur}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
{
|
||||
type === 'password' &&
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={inputVal}
|
||||
onChange={(_value, data) => handleChange(data.value)}
|
||||
onBlur={handleBlur}
|
||||
size="small"
|
||||
contentAfter={
|
||||
<Button
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
icon={showPassword ? <EyeRegular /> : <EyeOffRegular />}
|
||||
appearance="transparent"
|
||||
size="small"
|
||||
>
|
||||
</Button>}
|
||||
/>
|
||||
}
|
||||
{
|
||||
type === 'textarea' &&
|
||||
<Textarea
|
||||
value={inputVal}
|
||||
size="small"
|
||||
onChange={(_value, data) => handleChange(data.value)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectionInfoFormContainer = () => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
const classes = useStyles();
|
||||
|
||||
const generateFormComponent = (component: FormComponent, profile: IConnectionDialogProfile, _idx: number) => {
|
||||
switch (component.type) {
|
||||
case FormComponentType.Input:
|
||||
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='input' />;
|
||||
case FormComponentType.TextArea:
|
||||
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='textarea' />;
|
||||
case FormComponentType.Password:
|
||||
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='password' />;
|
||||
case FormComponentType.Dropdown:
|
||||
if (component.options === undefined) {
|
||||
throw new Error('Dropdown component must have options');
|
||||
}
|
||||
return <Dropdown
|
||||
size="small"
|
||||
placeholder={component.placeholder ?? ''}
|
||||
value={component.options.find(option => option.value === profile[component.propertyName])?.displayName ?? ''}
|
||||
selectedOptions={[profile[component.propertyName] as string]}
|
||||
onOptionSelect={(_event, data) => {
|
||||
connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: false,
|
||||
value: data.optionValue as string
|
||||
});
|
||||
}}>
|
||||
{
|
||||
component.options?.map((option, idx) => {
|
||||
return <Option key={component.propertyName + idx} value={option.value}>{option.displayName}</Option>
|
||||
})
|
||||
}
|
||||
</Dropdown>;
|
||||
case FormComponentType.Checkbox:
|
||||
return <Checkbox
|
||||
size="medium"
|
||||
checked={profile[component.propertyName] as boolean ?? false}
|
||||
onChange={(_value, data) => connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: false,
|
||||
value: data.checked
|
||||
})}
|
||||
/>;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
if (!connectionDialogContext?.state) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.formRoot}>
|
||||
<div style={
|
||||
{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}
|
||||
}>
|
||||
<Image style={
|
||||
{
|
||||
padding: '10px',
|
||||
}
|
||||
}
|
||||
src={connectionDialogContext?.theme === webLightTheme ? sqlServerImage : sqlServerImageDark} alt='SQL Server' height={60} width={60} />
|
||||
<Text size={500} style={
|
||||
{
|
||||
lineHeight: '60px'
|
||||
}
|
||||
} weight='medium'>Connect to SQL Server</Text>
|
||||
</div>
|
||||
<TabList selectedValue={connectionDialogContext?.state?.selectedFormTab ?? FormTabs.Parameters} onTabSelect={(_event, data) => {
|
||||
connectionDialogContext?.setFormTab(data.value as FormTabs);
|
||||
}}>
|
||||
<Tab value={FormTabs.Parameters}>Parameters</Tab>
|
||||
<Tab value={FormTabs.ConnectionString}>Connection String</Tab>
|
||||
</TabList>
|
||||
<div style={
|
||||
{
|
||||
overflow: 'auto'
|
||||
}
|
||||
}>
|
||||
<div className={classes.formDiv}>
|
||||
{
|
||||
connectionDialogContext?.state.formError &&
|
||||
<MessageBar intent="error">
|
||||
{connectionDialogContext.state.formError}
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
{
|
||||
connectionDialogContext.state.formComponents.map((component, idx) => {
|
||||
if (component.hidden === true) {
|
||||
return undefined;
|
||||
}
|
||||
return <div className={classes.formComponentDiv} key={idx}>
|
||||
<Field
|
||||
validationMessage={component.validation?.validationMessage ?? ''}
|
||||
orientation={component.type === FormComponentType.Checkbox ? 'horizontal' : 'vertical'}
|
||||
validationState={component.validation ? (component.validation.isValid ? 'none' : 'error') : 'none'}
|
||||
required={component.required}
|
||||
label={component.label}>
|
||||
{generateFormComponent(component, connectionDialogContext.state.connectionProfile, idx)}
|
||||
</Field>
|
||||
{
|
||||
component?.actionButtons?.length! > 0 &&
|
||||
<div className={classes.formComponentActionDiv}>
|
||||
{
|
||||
component.actionButtons?.map((actionButton, idx) => {
|
||||
return <Button shape="square" key={idx + actionButton.id} appearance='outline' style={
|
||||
{
|
||||
width: '120px'
|
||||
}
|
||||
} onClick={() => connectionDialogContext?.formAction({
|
||||
propertyName: component.propertyName,
|
||||
isAction: true,
|
||||
value: actionButton.id
|
||||
})}>{actionButton.label}</Button>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={connectionDialogContext.state.connectionStatus === ApiStatus.Loading}
|
||||
shape="square"
|
||||
onClick={(_event) => {
|
||||
connectionDialogContext.connect();
|
||||
}} style={
|
||||
{
|
||||
width: '200px',
|
||||
alignSelf: 'center'
|
||||
}
|
||||
}
|
||||
iconPosition="after"
|
||||
icon={ connectionDialogContext.state.connectionStatus === ApiStatus.Loading ? <Spinner size='tiny' /> : undefined}>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
import { Divider, makeStyles, shorthands } from "@fluentui/react-components";
|
||||
import { ResizableBox } from "react-resizable";
|
||||
import { MruConnectionsContainer } from "./mruConnectionsContainer";
|
||||
import { ConnectionInfoFormContainer } from "./connectionInfoFormContainer";
|
||||
import { ConnectionInfoFormContainer } from "./connectionPageContainer";
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
root: {
|
||||
|
@ -59,9 +59,7 @@ export const ConnectionPage = () => {
|
|||
maxConstraints={[800, Infinity]}
|
||||
minConstraints={[300, Infinity]}
|
||||
resizeHandles={['w']}
|
||||
handle={
|
||||
<div className={classes.mruPaneHandle} />
|
||||
}
|
||||
handle={<div className={classes.mruPaneHandle} /> }
|
||||
>
|
||||
<MruConnectionsContainer />
|
||||
</ResizableBox>
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ReactNode, useContext } from "react";
|
||||
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
|
||||
import { Tab, TabList, makeStyles } from "@fluentui/react-components";
|
||||
import { ConnectionDialogContextProps, FormTabType } from "../../../sharedInterfaces/connectionDialog";
|
||||
import './sqlServerRotation.css';
|
||||
import { ConnectionHeader } from "./connectionHeader";
|
||||
import { ConnectionFormPage } from "./connectionFormPage";
|
||||
import { ConnectionStringPage } from "./connectionStringPage";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
formRoot: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
formDiv: {
|
||||
padding: '10px',
|
||||
maxWidth: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
},
|
||||
formComponentDiv: {
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
},
|
||||
formComponentActionDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'> *': {
|
||||
margin: '5px',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function renderTab(connectionDialogContext: ConnectionDialogContextProps): ReactNode {
|
||||
switch (connectionDialogContext?.state.selectedFormTab) {
|
||||
case FormTabType.Parameters:
|
||||
return <ConnectionFormPage />;
|
||||
case FormTabType.ConnectionString:
|
||||
return <ConnectionStringPage />;
|
||||
}
|
||||
}
|
||||
|
||||
export const ConnectionInfoFormContainer = () => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
const classes = useStyles();
|
||||
|
||||
if (!connectionDialogContext?.state) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.formRoot}>
|
||||
<ConnectionHeader />
|
||||
<TabList
|
||||
selectedValue={connectionDialogContext?.state?.selectedFormTab ?? FormTabType.Parameters}
|
||||
onTabSelect={(_event, data) => { connectionDialogContext?.setFormTab(data.value as FormTabType); }}
|
||||
>
|
||||
<Tab value={FormTabType.Parameters}>Parameters</Tab>
|
||||
<Tab value={FormTabType.ConnectionString}>Connection String</Tab>
|
||||
</TabList>
|
||||
<div style={ { overflow: 'auto' } }>
|
||||
{ renderTab(connectionDialogContext) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Button, MessageBar, Spinner } from "@fluentui/react-components";
|
||||
import { ApiStatus } from "../../../sharedInterfaces/connectionDialog";
|
||||
import { useContext } from "react";
|
||||
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
|
||||
import { generateFormField, useFormStyles } from "../../common/forms/formUtils";
|
||||
|
||||
export const ConnectionStringPage = () => {
|
||||
const connectionDialogContext = useContext(ConnectionDialogContext);
|
||||
const formStyles = useFormStyles();
|
||||
|
||||
if (connectionDialogContext === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={formStyles.formDiv}>
|
||||
{
|
||||
connectionDialogContext?.state.formError &&
|
||||
<MessageBar intent="error">
|
||||
{connectionDialogContext.state.formError}
|
||||
</MessageBar>
|
||||
}
|
||||
{
|
||||
connectionDialogContext.state.connectionStringComponents.map((component, idx) => {
|
||||
if (component.hidden === true) {
|
||||
return undefined;
|
||||
}
|
||||
return generateFormField(connectionDialogContext, component, idx, formStyles);
|
||||
})
|
||||
}
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={connectionDialogContext.state.connectionStatus === ApiStatus.Loading}
|
||||
shape="square"
|
||||
onClick={(_event) => {
|
||||
connectionDialogContext.connect();
|
||||
}} style={
|
||||
{
|
||||
width: '200px',
|
||||
alignSelf: 'center'
|
||||
}
|
||||
}
|
||||
iconPosition="after"
|
||||
icon={ connectionDialogContext.state.connectionStatus === ApiStatus.Loading ? <Spinner size='tiny' /> : undefined}>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -56,10 +56,10 @@ export const MruConnectionsContainer = () => {
|
|||
<TreeItemLayout iconBefore={<ServerRegular />}>
|
||||
{connection.profileName}
|
||||
</TreeItemLayout>
|
||||
</TreeItem>
|
||||
</TreeItem>;
|
||||
})
|
||||
}
|
||||
</Tree>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,7 +6,20 @@
|
|||
import { Theme } from "@fluentui/react-components";
|
||||
import * as vscodeMssql from "vscode-mssql";
|
||||
|
||||
export enum FormTabs {
|
||||
export interface ConnectionDialogWebviewState {
|
||||
selectedFormTab: FormTabType;
|
||||
connectionFormComponents: {
|
||||
mainComponents: FormComponent[];
|
||||
advancedComponents: {[category: string]: FormComponent[]};
|
||||
};
|
||||
connectionStringComponents: FormComponent[];
|
||||
recentConnections: IConnectionDialogProfile[];
|
||||
connectionProfile: IConnectionDialogProfile;
|
||||
connectionStatus: ApiStatus;
|
||||
formError: string;
|
||||
}
|
||||
|
||||
export enum FormTabType {
|
||||
Parameters = 'parameter',
|
||||
ConnectionString = 'connString'
|
||||
}
|
||||
|
@ -20,15 +33,6 @@ export interface IConnectionDialogProfile extends vscodeMssql.IConnectionInfo {
|
|||
azureAuthType?: vscodeMssql.AzureAuthType;
|
||||
}
|
||||
|
||||
export interface ConnectionDialogWebviewState {
|
||||
selectedFormTab: FormTabs;
|
||||
recentConnections: IConnectionDialogProfile[];
|
||||
formComponents: FormComponent[];
|
||||
connectionProfile: IConnectionDialogProfile;
|
||||
connectionStatus: ApiStatus;
|
||||
formError: string;
|
||||
}
|
||||
|
||||
export enum ApiStatus {
|
||||
NotStarted = 'notStarted',
|
||||
Loading = 'loading',
|
||||
|
@ -36,12 +40,15 @@ export enum ApiStatus {
|
|||
Error = 'error',
|
||||
}
|
||||
|
||||
export interface ConnectionDialogContextProps {
|
||||
export interface FormContextProps<T> {
|
||||
formAction: (event: FormEvent<T>) => void;
|
||||
}
|
||||
|
||||
export interface ConnectionDialogContextProps extends FormContextProps<IConnectionDialogProfile> {
|
||||
state: ConnectionDialogWebviewState;
|
||||
theme: Theme;
|
||||
loadConnection: (connection: IConnectionDialogProfile) => void;
|
||||
formAction: (event: FormEvent) => void;
|
||||
setFormTab: (tab: FormTabs) => void;
|
||||
setFormTab: (tab: FormTabType) => void;
|
||||
connect: () => void;
|
||||
}
|
||||
|
||||
|
@ -122,11 +129,11 @@ export interface FormComponentOptions {
|
|||
/**
|
||||
* Interface for a form event
|
||||
*/
|
||||
export interface FormEvent {
|
||||
export interface FormEvent<T> {
|
||||
/**
|
||||
* The property name of the form component that triggered the event
|
||||
*/
|
||||
propertyName: keyof IConnectionDialogProfile;
|
||||
propertyName: keyof T;
|
||||
/**
|
||||
* Whether the event was triggered by an action button for the component
|
||||
*/
|
||||
|
@ -158,10 +165,10 @@ export enum AuthenticationType {
|
|||
|
||||
export interface ConnectionDialogReducers {
|
||||
setFormTab: {
|
||||
tab: FormTabs;
|
||||
tab: FormTabType;
|
||||
},
|
||||
formAction: {
|
||||
event: FormEvent;
|
||||
event: FormEvent<IConnectionDialogProfile>;
|
||||
},
|
||||
loadConnection: {
|
||||
connection: IConnectionDialogProfile;
|
||||
|
|
1148
yarn.lock
1148
yarn.lock
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче