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:
Benjin Dubishar 2024-08-26 13:23:53 -07:00 коммит произвёл GitHub
Родитель 1aad8cdc0a
Коммит 3dc342e71a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
20 изменённых файлов: 1581 добавлений и 948 удалений

Просмотреть файл

@ -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

Разница между файлами не показана из-за своего большого размера Загрузить разницу