diff --git a/extensions/mssql/src/resourceProvider/contracts.ts b/extensions/mssql/src/resourceProvider/contracts.ts index 5bae70aab6..17a704769f 100644 --- a/extensions/mssql/src/resourceProvider/contracts.ts +++ b/extensions/mssql/src/resourceProvider/contracts.ts @@ -7,35 +7,98 @@ import { RequestType } from 'vscode-languageclient'; import * as azdata from 'azdata'; // ------------------------------- < Resource Events > ------------------------------------ + +/** + * A request to open up a firewall rule + */ export namespace CreateFirewallRuleRequest { export const type = new RequestType('resource/createFirewallRule'); } +/** + * Firewall rule request handler + */ export namespace HandleFirewallRuleRequest { export const type = new RequestType('resource/handleFirewallRule'); } -// Firewall rule interfaces +/** + * Firewall rule creation parameters + */ export interface CreateFirewallRuleParams { + /** + * Account information to use in connecting to Azure + */ account: azdata.Account; + /** + * Fully qualified name of the server to create a new firewall rule on + */ serverName: string; + /** + * Firewall rule name to set + */ + firewallRuleName: string; + /** + * Start of the IP address range + */ startIpAddress: string; + /** + * End of the IP address range + */ endIpAddress: string; + /** + * Per-tenant token mappings. Ideally would be set independently of this call, + * but for now this allows us to get the tokens necessary to find a server and open a firewall rule + */ securityTokenMappings: {}; } +/** + * Firewall rule creation response + */ interface CreateFirewallRuleResponse { + /** + * Whether or not request can be handled. + */ result: boolean; + /** + * Contains error message, if request could not be handled. + */ errorMessage: string; } +/** + * Firewall rule handling parameters + */ export interface HandleFirewallRuleParams { + /** + * The error code used to defined the error type + */ errorCode: number; + /** + * The error message from which to parse the IP address + */ errorMessage: string; + /** + * The connection type, for example MSSQL + */ connectionTypeId: string; } +/** + * Response to the check for Firewall rule support given an error message + */ interface HandleFirewallRuleResponse { + /** + * Whether or not request can be handled. + */ result: boolean; + /** + * Contains error message, if request could not be handled. + */ + errorMessage: string; + /** + * If handled, the default IP address to send back; so users can tell what their blocked IP is. + */ ipAddress: string; } diff --git a/extensions/mssql/src/resourceProvider/resourceProvider.ts b/extensions/mssql/src/resourceProvider/resourceProvider.ts index 3a3e5fd238..db74802c1f 100644 --- a/extensions/mssql/src/resourceProvider/resourceProvider.ts +++ b/extensions/mssql/src/resourceProvider/resourceProvider.ts @@ -63,6 +63,7 @@ class FireWallFeature extends SqlOpsFeature { function asCreateFirewallRuleParams(account: azdata.Account, params: azdata.FirewallRuleInfo): CreateFirewallRuleParams { return { account: account, + firewallRuleName: params.firewallRuleName, serverName: params.serverName, startIpAddress: params.startIpAddress, endIpAddress: params.endIpAddress, diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index c75207808e..58a969f117 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -2642,24 +2642,78 @@ declare module 'azdata' { * Represents a provider of resource */ export interface ResourceProvider { + /** + * Creates a firewall rule for the given account + * @param account Account with which firewall rule request will be made. + * @param firewallruleInfo Firewall rule creation information + */ createFirewallRule(account: Account, firewallruleInfo: FirewallRuleInfo): Thenable; + + /** + * Handles the response from the firewall rule creation request + * @param errorCode Error code from the firewall rule creation request + * @param errorMessage Error message from the firewall rule creation request + * @param connectionTypeId Connection type id of the firewall rule creation request + */ handleFirewallRule(errorCode: number, errorMessage: string, connectionTypeId: string): Thenable; } + /** + * Firewall rule creation information + */ export interface FirewallRuleInfo { + /** + * Start of the IP address range + */ startIpAddress?: string | undefined; + /** + * End of the IP address range + */ endIpAddress?: string | undefined; + /** + * Fully qualified name of the server to create a new firewall rule on + */ serverName: string; + /** + * Firewall rule name to set + */ + firewallRuleName: string; + /** + * Per-tenant token mappings. Ideally would be set independently of this call, + * but for now this allows us to get the tokens necessary to find a server and open a firewall rule + */ securityTokenMappings: {}; } + /** + * Firewall rule creation response + */ export interface CreateFirewallRuleResponse { + /** + * Whether or not request can be handled. + */ result: boolean; + /** + * Contains error message, if request could not be handled. + */ errorMessage: string; } + /** + * Response to the check for Firewall rule support given an error message + */ export interface HandleFirewallRuleResponse { + /** + * Whether or not request can be handled. + */ result: boolean; + /** + * Contains error message, if request could not be handled. + */ + errorMessage: string; + /** + * If handled, the default IP address to send back; so users can tell what their blocked IP is. + */ ipAddress: string; } diff --git a/src/sql/base/browser/ui/dropdownList/dropdownList.ts b/src/sql/base/browser/ui/dropdownList/dropdownList.ts index 8fc398fc22..c78c1881c2 100644 --- a/src/sql/base/browser/ui/dropdownList/dropdownList.ts +++ b/src/sql/base/browser/ui/dropdownList/dropdownList.ts @@ -27,6 +27,7 @@ export class DropdownList extends Dropdown { protected backgroundColor?: Color; protected foregroundColor?: Color; protected borderColor?: Color; + protected borderWidth = 1; private button?: Button; @@ -89,7 +90,7 @@ export class DropdownList extends Dropdown { */ protected override renderContents(container: HTMLElement): IDisposable { let div = DOM.append(container, this._contentContainer); - div.style.width = DOM.getTotalWidth(this.element) + 'px'; + div.style.width = (DOM.getTotalWidth(this.element) - this.borderWidth * 2) + 'px'; // Subtract border width return { dispose: () => { } }; } @@ -145,7 +146,8 @@ export class DropdownList extends Dropdown { element.style.backgroundColor = background; element.style.color = foreground; - element.style.borderWidth = border ? '1px' : ''; + this.borderWidth = border ? 1 : 0; + element.style.borderWidth = border ? this.borderWidth + 'px' : ''; element.style.borderStyle = border ? 'solid' : ''; element.style.borderColor = border; } diff --git a/src/sql/platform/accounts/common/firewallRuleViewModel.ts b/src/sql/platform/accounts/common/firewallRuleViewModel.ts index 0bd967a390..80f1575e6b 100644 --- a/src/sql/platform/accounts/common/firewallRuleViewModel.ts +++ b/src/sql/platform/accounts/common/firewallRuleViewModel.ts @@ -13,6 +13,8 @@ export class FirewallRuleViewModel { public selectedAccount: azdata.Account | undefined; public selectedTenantId: string | undefined; + private _defaultFirewallRuleName: string; + private _firewallRuleName: string; private _defaultIPAddress?: string; private _defaultFromSubnetIPRange?: string; private _defaultToSubnetIPRange?: string; @@ -23,6 +25,22 @@ export class FirewallRuleViewModel { this.isIPAddressSelected = true; } + public set defaultFirewallRuleName(ruleName: string) { + this._defaultFirewallRuleName = ruleName; + } + + public get defaultFirewallRuleName(): string | undefined { + return this._defaultFirewallRuleName; + } + + public set firewallRuleName(ruleName: string) { + this._firewallRuleName = ruleName; + } + + public get firewallRuleName(): string | undefined { + return this._firewallRuleName; + } + public get defaultIPAddress(): string | undefined { return this._defaultIPAddress; } @@ -60,6 +78,29 @@ export class FirewallRuleViewModel { } public updateDefaultValues(ipAddress: string): void { + function padTo2Digits(num: number) { + return num.toString().padStart(2, '0'); + } + + // format as "YYYY-MM-DD_hh-mm-ss" (default Azure rulename format) + function formatDate(date: Date) { + return ( + [ + date.getFullYear(), + padTo2Digits(date.getMonth() + 1), + padTo2Digits(date.getDate()), + ].join('-') + + '_' + + [ + padTo2Digits(date.getHours()), + padTo2Digits(date.getMinutes()), + padTo2Digits(date.getSeconds()), + ].join('-') + ); + } + + // Use default rule name format as Azure portal. + this._defaultFirewallRuleName = `ClientIPAddress_${formatDate(new Date())}`; this._defaultIPAddress = ipAddress; this._defaultFromSubnetIPRange = ipAddress.replace(/\.[0-9]+$/g, '.0'); this._defaultToSubnetIPRange = ipAddress.replace(/\.[0-9]+$/g, '.255'); diff --git a/src/sql/workbench/services/accountManagement/browser/accountPickerImpl.ts b/src/sql/workbench/services/accountManagement/browser/accountPickerImpl.ts index 322085d4f8..86b8b37797 100644 --- a/src/sql/workbench/services/accountManagement/browser/accountPickerImpl.ts +++ b/src/sql/workbench/services/accountManagement/browser/accountPickerImpl.ts @@ -28,6 +28,7 @@ import { Tenant, TenantListDelegate, TenantPickerListRenderer } from 'sql/workbe export class AccountPicker extends Disposable { public static ACCOUNTPICKERLIST_HEIGHT = 47; + public static ACCOUNTTENANTLIST_HEIGHT = 32; public viewModel: AccountPickerViewModel; private _accountList?: List; private _rootContainer?: HTMLElement; @@ -97,7 +98,7 @@ export class AccountPicker extends Disposable { public createAccountPickerComponent() { // Create an account list const accountDelegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT); - const tenantDelegate = new TenantListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT); + const tenantDelegate = new TenantListDelegate(AccountPicker.ACCOUNTTENANTLIST_HEIGHT); const accountRenderer = new AccountPickerListRenderer(); const tenantRenderer = new TenantPickerListRenderer(); @@ -203,7 +204,7 @@ export class AccountPicker extends Disposable { private createLabelElement(content: string, isHeader?: boolean) { let className = 'dialog-label'; if (isHeader) { - className += ' header'; + className += '.header'; } const element = DOM.$(`.${className}`); element.innerText = content; diff --git a/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css b/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css index 819ceda47e..f72a84eca9 100644 --- a/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css +++ b/src/sql/workbench/services/accountManagement/browser/media/accountListRenderer.css @@ -1,4 +1,4 @@ -/*--------------------------------------------------------------------------------------------- +/*-------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ @@ -12,17 +12,18 @@ .list-row.account-picker-list .label, .list-row.tenant-picker-list .label { flex: 1 1 auto; - margin-left: 15px; + margin-left: 12px; overflow: hidden; } .list-row.account-picker-list .label .contextual-display-name { - font-size: 15px; + font-size: 13px; } .list-row.account-picker-list .label .display-name, .list-row.tenant-picker-list .label .display-name { - font-size: 13px; + font-size: 12px; + line-height: 1.5; } .list-row.account-picker-list .label .content, diff --git a/src/sql/workbench/services/accountManagement/browser/media/accountPicker.css b/src/sql/workbench/services/accountManagement/browser/media/accountPicker.css index 48ec37ba12..7f20a79d22 100644 --- a/src/sql/workbench/services/accountManagement/browser/media/accountPicker.css +++ b/src/sql/workbench/services/accountManagement/browser/media/accountPicker.css @@ -57,10 +57,10 @@ } .account-list-container .list-row .codicon { - flex: 0 0 35px; - height: 35px; - width: 35px; - background-size: 35px; + flex: 0 0 32px; + height: 32px; + width: 32px; + background-size: 32px; } .account-list-container .list-row .codicon .badge { diff --git a/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts b/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts index e9f11a9bee..5b53d8ca1e 100644 --- a/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts +++ b/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialog.ts @@ -38,13 +38,24 @@ const firewallHelpUri = 'https://aka.ms/sqlopsfirewallhelp'; const LocalizedStrings = { FROM: localize('from', "From"), - TO: localize('to', "To") + TO: localize('to', "To"), + OK: localize('firewall.ok', "OK"), + Cancel: localize('firewall.cancel', "Cancel"), + RuleName: localize('firewall.ruleName', "Rule name"), + CreateNewFirewallRule: localize('createNewFirewallRule', "Create new firewall rule"), + FirewallRuleLabel: localize('filewallRule', "Firewall rule"), + FirewallRuleDescription: localize('firewallRuleDescription', + "A firewall rule is required to access the SQL Server instance. Click the link below to create a new firewall rule."), + FirewallRuleHelpLink: localize('firewallRuleHelpLink', "Learn more about firewall rules"), + AddClientIPLabel: localize('addIPAddressLabel', "Add my client IP "), + AddIPRangeLabel: localize('addIpRangeLabel', "Add my subnet IP range") }; export class FirewallRuleDialog extends Modal { public viewModel: FirewallRuleViewModel; private _createButton?: Button; private _closeButton?: Button; + private _ruleNameInpuBox?: InputBox; private _fromRangeinputBox?: InputBox; private _toRangeinputBox?: InputBox; @@ -77,7 +88,7 @@ export class FirewallRuleDialog extends Modal { @IOpenerService private readonly openerService: IOpenerService ) { super( - localize('createNewFirewallRule', "Create new firewall rule"), + LocalizedStrings.CreateNewFirewallRule, TelemetryKeys.ModalDialogName.FireWallRule, telemetryService, layoutService, @@ -106,30 +117,97 @@ export class FirewallRuleDialog extends Modal { attachModalDialogStyler(this, this._themeService); this.backButton!.onDidClick(() => this.cancel()); this._register(attachButtonStyler(this.backButton!, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND })); - this._createButton = this.addFooterButton(localize('firewall.ok', "OK"), () => this.createFirewallRule()); - this._closeButton = this.addFooterButton(localize('firewall.cancel', "Cancel"), () => this.cancel(), 'right', true); + this._createButton = this.addFooterButton(LocalizedStrings.OK, () => this.createFirewallRule()); + this._closeButton = this.addFooterButton(LocalizedStrings.Cancel, () => this.cancel(), 'right', true); this.registerListeners(); } protected renderBody(container: HTMLElement) { - const body = DOM.append(container, DOM.$('.firewall-rule-dialog')); - const descriptionSection = DOM.append(body, DOM.$('.firewall-rule-description-section.new-section')); + const dialogBody = DOM.append(container, DOM.$('.firewall-rule-dialog')); + this.createFirewallRuleHeader(dialogBody); + this.createAccountPicker(dialogBody); + + // Firewall rule section + const firewallRuleSection = DOM.append(dialogBody, DOM.$('.firewall-rule-section.new-section')); + this.createLabelElement(firewallRuleSection, LocalizedStrings.FirewallRuleLabel, true); + const radioContainer = DOM.append(firewallRuleSection, DOM.$('.radio-section')); + const form = DOM.append(radioContainer, DOM.$('form.firewall-rule')); + + // Firewall rule name inputBox + const descriptionDiv = DOM.append(form, DOM.$('div.firewall-rulename dialog-input')); + const descInputContainer = DOM.append(descriptionDiv, DOM.$('.dialog-input-section')); + DOM.append(descInputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.RuleName; + this._ruleNameInpuBox = new InputBox(DOM.append(descInputContainer, DOM.$('.dialog-input')), this._contextViewService, { + ariaLabel: LocalizedStrings.RuleName + }); + + // Single IP Address radio button + const IPAddressDiv = DOM.append(form, DOM.$('div.firewall-ip-address dialog-input')); + const subnetIPRangeDiv = DOM.append(form, DOM.$('div.firewall-subnet-ip-range dialog-input')); + const IPAddressContainer = DOM.append(IPAddressDiv, DOM.$('div.option-container')); + this._IPAddressInput = DOM.append(IPAddressContainer, DOM.$('input.option-input')); + this._IPAddressInput.setAttribute('type', 'radio'); + this._IPAddressInput.setAttribute('name', 'firewallRuleChoice'); + this._IPAddressInput.setAttribute('value', 'ipAddress'); + const IPAddressDescription = DOM.append(IPAddressContainer, DOM.$('div.option-description')); + IPAddressDescription.innerText = LocalizedStrings.AddClientIPLabel; + this._IPAddressElement = DOM.append(IPAddressContainer, DOM.$('div.option-ip-address')); + + // IP Range radio button + const subnetIpRangeContainer = DOM.append(subnetIPRangeDiv, DOM.$('div.option-container')); + this._subnetIPRangeInput = DOM.append(subnetIpRangeContainer, DOM.$('input.option-input')); + this._subnetIPRangeInput.setAttribute('type', 'radio'); + this._subnetIPRangeInput.setAttribute('name', 'firewallRuleChoice'); + this._subnetIPRangeInput.setAttribute('value', 'ipRange'); + const subnetIPRangeDescription = DOM.append(subnetIpRangeContainer, DOM.$('div.option-description')); + subnetIPRangeDescription.innerText = LocalizedStrings.AddIPRangeLabel; + + // IP Range input boxes + const subnetIPRangeSection = DOM.append(subnetIPRangeDiv, DOM.$('.subnet-ip-range-input')); + const inputContainer = DOM.append(subnetIPRangeSection, DOM.$('.dialog-input-section')); + + DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.FROM; + this._fromRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, { + ariaLabel: LocalizedStrings.FROM + }); + + DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.TO; + this._toRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, { + ariaLabel: LocalizedStrings.TO + }); + + // Register events + this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e))); + this.updateTheme(this._themeService.getColorTheme()); + + this._register(DOM.addDisposableListener(this._IPAddressInput, DOM.EventType.CLICK, () => { + this.onFirewallRuleOptionSelected(true); + })); + + this._register(DOM.addDisposableListener(this._subnetIPRangeInput, DOM.EventType.CLICK, () => { + this.onFirewallRuleOptionSelected(false); + })); + } + + // Create firewall rule header + private createFirewallRuleHeader(dialogBody: HTMLElement) { + const descriptionSection = DOM.append(dialogBody, DOM.$('.firewall-rule-description-section.new-section')); DOM.append(descriptionSection, DOM.$('div.firewall-rule-icon')); - const textDescriptionContainer = DOM.append(descriptionSection, DOM.$('div.firewall-rule-description')); - const dialogDescription = localize('firewallRuleDialogDescription', - "Your client IP address does not have access to the server. Sign in to an Azure account and create a new firewall rule to enable access."); - this.createLabelElement(textDescriptionContainer, dialogDescription, false); + this.createLabelElement(textDescriptionContainer, LocalizedStrings.FirewallRuleDescription, false); this._helpLink = DOM.append(textDescriptionContainer, DOM.$('a.help-link')); + this._helpLink.setAttribute('href', firewallHelpUri); - this._helpLink.innerHTML += localize('firewallRuleHelpDescription', "Learn more about firewall settings"); + this._helpLink.innerHTML += LocalizedStrings.FirewallRuleHelpLink; this._helpLink.onclick = () => { this.openerService.open(URI.parse(firewallHelpUri)); }; + } - // Create account picker with event handling + // Create account picker with event handling + private createAccountPicker(dialogBody: HTMLElement) { this._accountPickerService.addAccountCompleteEvent(() => this.spinner = false); this._accountPickerService.addAccountErrorEvent((msg) => { this.spinner = false; @@ -139,68 +217,22 @@ export class FirewallRuleDialog extends Modal { this._accountPickerService.onAccountSelectionChangeEvent((account) => this.onAccountSelectionChange(account)); this._accountPickerService.onTenantSelectionChangeEvent((tenantId) => !!tenantId && this.onTenantSelectionChange(tenantId)); - const azureAccountSection = DOM.append(body, DOM.$('.azure-account-section.new-section')); + const azureAccountSection = DOM.append(dialogBody, DOM.$('.azure-account-section.new-section')); this._accountPickerService.renderAccountPicker(azureAccountSection); - - const firewallRuleSection = DOM.append(body, DOM.$('.firewall-rule-section.new-section')); - const firewallRuleLabel = localize('filewallRule', "Firewall rule"); - this.createLabelElement(firewallRuleSection, firewallRuleLabel, true); - const radioContainer = DOM.append(firewallRuleSection, DOM.$('.radio-section')); - const form = DOM.append(radioContainer, DOM.$('form.firewall-rule')); - const IPAddressDiv = DOM.append(form, DOM.$('div.firewall-ip-address dialog-input')); - const subnetIPRangeDiv = DOM.append(form, DOM.$('div.firewall-subnet-ip-range dialog-input')); - - const IPAddressContainer = DOM.append(IPAddressDiv, DOM.$('div.option-container')); - this._IPAddressInput = DOM.append(IPAddressContainer, DOM.$('input.option-input')); - this._IPAddressInput.setAttribute('type', 'radio'); - this._IPAddressInput.setAttribute('name', 'firewallRuleChoice'); - this._IPAddressInput.setAttribute('value', 'ipAddress'); - const IPAddressDescription = DOM.append(IPAddressContainer, DOM.$('div.option-description')); - IPAddressDescription.innerText = localize('addIPAddressLabel', "Add my client IP "); - this._IPAddressElement = DOM.append(IPAddressContainer, DOM.$('div.option-ip-address')); - - const subnetIpRangeContainer = DOM.append(subnetIPRangeDiv, DOM.$('div.option-container')); - this._subnetIPRangeInput = DOM.append(subnetIpRangeContainer, DOM.$('input.option-input')); - this._subnetIPRangeInput.setAttribute('type', 'radio'); - this._subnetIPRangeInput.setAttribute('name', 'firewallRuleChoice'); - this._subnetIPRangeInput.setAttribute('value', 'ipRange'); - const subnetIPRangeDescription = DOM.append(subnetIpRangeContainer, DOM.$('div.option-description')); - subnetIPRangeDescription.innerText = localize('addIpRangeLabel', "Add my subnet IP range"); - const subnetIPRangeSection = DOM.append(subnetIPRangeDiv, DOM.$('.subnet-ip-range-input')); - - const inputContainer = DOM.append(subnetIPRangeSection, DOM.$('.dialog-input-section')); - - DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.FROM; - - this._fromRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, { - ariaLabel: LocalizedStrings.FROM - }); - - DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.TO; - - this._toRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, { - ariaLabel: LocalizedStrings.TO - }); - - this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e))); - this.updateTheme(this._themeService.getColorTheme()); - - this._register(DOM.addDisposableListener(this._IPAddressElement, DOM.EventType.CLICK, () => { - this.onFirewallRuleOptionSelected(true); - })); - - this._register(DOM.addDisposableListener(this._subnetIPRangeInput, DOM.EventType.CLICK, () => { - this.onFirewallRuleOptionSelected(false); - })); } private onFirewallRuleOptionSelected(isIPAddress: boolean) { this.viewModel.isIPAddressSelected = isIPAddress; - if (this._fromRangeinputBox) { - isIPAddress ? this._fromRangeinputBox.disable() : this._fromRangeinputBox.enable(); - } - if (this._toRangeinputBox) { - isIPAddress ? this._toRangeinputBox.disable() : this._toRangeinputBox.enable(); + if (isIPAddress) { + this._fromRangeinputBox!.disable(); + this._fromRangeinputBox!.value = ''; + this._toRangeinputBox!.disable(); + this._toRangeinputBox!.value = ''; + } else { + this._fromRangeinputBox!.enable(); + this._fromRangeinputBox!.value = this.viewModel!.defaultFromSubnetIPRange ?? ''; + this._toRangeinputBox!.enable(); + this._toRangeinputBox!.value = this.viewModel!.defaultToSubnetIPRange ?? ''; } } @@ -211,7 +243,7 @@ export class FirewallRuleDialog extends Modal { private createLabelElement(container: HTMLElement, content: string, isHeader?: boolean) { let className = 'dialog-label'; if (isHeader) { - className += ' header'; + className += '.header'; } const element = DOM.append(container, DOM.$(`.${className}`)); element.innerText = content; @@ -231,9 +263,15 @@ export class FirewallRuleDialog extends Modal { // Theme styler this._register(attachButtonStyler(this._createButton!, this._themeService)); this._register(attachButtonStyler(this._closeButton!, this._themeService)); + this._register(attachInputBoxStyler(this._ruleNameInpuBox!, this._themeService)); this._register(attachInputBoxStyler(this._fromRangeinputBox!, this._themeService)); this._register(attachInputBoxStyler(this._toRangeinputBox!, this._themeService)); + // handler for firewall rule name change events + this._register(this._ruleNameInpuBox!.onDidChange(ruleName => { + this.firewallRuleNameChanged(ruleName); + })); + // handler for from subnet ip range change events this._register(this._fromRangeinputBox!.onDidChange(IPAddress => { this.fromRangeInputChanged(IPAddress); @@ -245,6 +283,10 @@ export class FirewallRuleDialog extends Modal { })); } + private firewallRuleNameChanged(ruleName: string) { + this.viewModel.firewallRuleName = ruleName; + } + private fromRangeInputChanged(IPAddress: string) { this.viewModel.fromSubnetIPRange = IPAddress; } @@ -301,6 +343,7 @@ export class FirewallRuleDialog extends Modal { public open() { this._IPAddressInput!.click(); this.onAccountSelectionChange(this._accountPickerService.selectedAccount); + this._ruleNameInpuBox!.value = this.viewModel!.defaultFirewallRuleName ?? ''; this._fromRangeinputBox!.setPlaceHolder(this.viewModel!.defaultFromSubnetIPRange ?? ''); this._toRangeinputBox!.setPlaceHolder(this.viewModel!.defaultToSubnetIPRange ?? ''); this._IPAddressElement!.innerText = `(${this.viewModel.defaultIPAddress ?? ''})`; diff --git a/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialogController.ts b/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialogController.ts index f2fa459655..2d35f399f3 100644 --- a/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialogController.ts +++ b/src/sql/workbench/services/resourceProvider/browser/firewallRuleDialogController.ts @@ -68,6 +68,7 @@ export class FirewallRuleDialogController { }; const firewallRuleInfo: azdata.FirewallRuleInfo = { + firewallRuleName: this._firewallRuleDialog!.viewModel.firewallRuleName, startIpAddress: this._firewallRuleDialog!.viewModel.isIPAddressSelected ? this._firewallRuleDialog!.viewModel.defaultIPAddress : this._firewallRuleDialog!.viewModel.fromSubnetIPRange, endIpAddress: this._firewallRuleDialog!.viewModel.isIPAddressSelected ? this._firewallRuleDialog!.viewModel.defaultIPAddress : this._firewallRuleDialog!.viewModel.toSubnetIPRange, serverName: this._connection!.serverName, diff --git a/src/sql/workbench/services/resourceProvider/browser/media/firewallRuleDialog.css b/src/sql/workbench/services/resourceProvider/browser/media/firewallRuleDialog.css index f4686fc513..df41c64558 100644 --- a/src/sql/workbench/services/resourceProvider/browser/media/firewallRuleDialog.css +++ b/src/sql/workbench/services/resourceProvider/browser/media/firewallRuleDialog.css @@ -8,7 +8,8 @@ } .modal .firewall-rule-dialog .dialog-label.header { - font-size: 15px; + font-size: 12px; + padding-top: 10px; } .modal .firewall-rule-dialog .new-section { @@ -29,8 +30,14 @@ padding-bottom: 10px; } +.modal .firewall-rule-dialog .firewall-rulename .dialog-input-section{ + display: block; + padding-top: 0px; +} + .modal .firewall-rule-dialog .dialog-input-section .dialog-label { - flex: 0 0 15px; + flex: 0 0; + padding-top: 3px; align-self: center; } @@ -42,6 +49,10 @@ padding-bottom: 0px; } +.modal .firewall-rule-dialog .firewall-rulename .dialog-input-section .dialog-input { + padding-left: 0px; +} + /* Firewall rule description section */ .modal .firewall-rule-dialog a:link { text-decoration: underline;