/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/firewallRuleDialog'; import * as DOM from 'vs/base/browser/dom'; import { Event, Emitter } from 'vs/base/common/event'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { localize } from 'vs/nls'; import { buttonBackground } from 'vs/platform/theme/common/colorRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; import { URI } from 'vs/base/common/uri'; import * as azdata from 'azdata'; import { Button } from 'sql/base/browser/ui/button/button'; import { Modal } from 'sql/workbench/browser/modal/modal'; import { FirewallRuleViewModel } from 'sql/platform/accounts/common/firewallRuleViewModel'; import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import { IAccountPickerService } from 'sql/workbench/services/accountManagement/browser/accountPicker'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { ILogService } from 'vs/platform/log/common/log'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { attachModalDialogStyler } from 'sql/workbench/common/styler'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; // TODO: Make the help link 1) extensible (01/08/2018, https://github.com/Microsoft/azuredatastudio/issues/450) // in case that other non-Azure sign in is to be used const firewallHelpUri = 'https://aka.ms/sqlopsfirewallhelp'; const LocalizedStrings = { FROM: localize('from', "From"), 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; private _helpLink?: HTMLElement; private _IPAddressInput?: HTMLElement; private _subnetIPRangeInput?: HTMLElement; private _IPAddressElement?: HTMLElement; // EVENTING //////////////////////////////////////////////////////////// private _onAddAccountErrorEmitter: Emitter; public get onAddAccountErrorEvent(): Event { return this._onAddAccountErrorEmitter.event; } private _onCancel: Emitter; public get onCancel(): Event { return this._onCancel.event; } private _onCreateFirewallRule: Emitter; public get onCreateFirewallRule(): Event { return this._onCreateFirewallRule.event; } constructor( @IAccountPickerService private _accountPickerService: IAccountPickerService, @ILayoutService layoutService: ILayoutService, @IThemeService themeService: IThemeService, @IInstantiationService private _instantiationService: IInstantiationService, @IContextViewService private _contextViewService: IContextViewService, @IAdsTelemetryService telemetryService: IAdsTelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @IClipboardService clipboardService: IClipboardService, @ILogService logService: ILogService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, @IOpenerService private readonly openerService: IOpenerService ) { super( LocalizedStrings.CreateNewFirewallRule, TelemetryKeys.ModalDialogName.FireWallRule, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'flyout', hasBackButton: true, hasSpinner: true } ); // Setup event emitters this._onAddAccountErrorEmitter = new Emitter(); this._onCancel = new Emitter(); this._onCreateFirewallRule = new Emitter(); this.viewModel = this._instantiationService.createInstance(FirewallRuleViewModel); } public override render() { super.render(); attachModalDialogStyler(this, this._themeService); this.backButton!.onDidClick(() => this.cancel()); this._register(this.backButton!); 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 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, inputBoxStyles: defaultInputBoxStyles }); // 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, inputBoxStyles: defaultInputBoxStyles }); DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.TO; this._toRangeinputBox = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, { ariaLabel: LocalizedStrings.TO, inputBoxStyles: defaultInputBoxStyles }); // 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')); this.createLabelElement(textDescriptionContainer, LocalizedStrings.FirewallRuleDescription, false); this._helpLink = DOM.append(textDescriptionContainer, DOM.$('a.help-link')); this._helpLink.setAttribute('href', firewallHelpUri); this._helpLink.innerHTML += LocalizedStrings.FirewallRuleHelpLink; this._helpLink.onclick = () => { this.openerService.open(URI.parse(firewallHelpUri)); }; } // Create account picker with event handling private createAccountPicker(dialogBody: HTMLElement) { this._accountPickerService.addAccountCompleteEvent(() => this.spinner = false); this._accountPickerService.addAccountErrorEvent((msg) => { this.spinner = false; this._onAddAccountErrorEmitter.fire(msg); }); this._accountPickerService.addAccountStartEvent(() => this.spinner = true); this._accountPickerService.onAccountSelectionChangeEvent((account) => this.onAccountSelectionChange(account)); this._accountPickerService.onTenantSelectionChangeEvent((tenantId) => !!tenantId && this.onTenantSelectionChange(tenantId)); const azureAccountSection = DOM.append(dialogBody, DOM.$('.azure-account-section.new-section')); this._accountPickerService.renderAccountPicker(azureAccountSection); } private onFirewallRuleOptionSelected(isIPAddress: boolean) { this.viewModel.isIPAddressSelected = isIPAddress; 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 ?? ''; } } protected layout(height?: number): void { // Nothing currently laid out statically in this class } private createLabelElement(container: HTMLElement, content: string, isHeader?: boolean) { let className = 'dialog-label'; if (isHeader) { className += '.header'; } const element = DOM.append(container, DOM.$(`.${className}`)); element.innerText = content; return element; } // Update theming that is specific to firewall rule flyout body private updateTheme(theme: IColorTheme): void { const linkColor = theme.getColor(buttonBackground); const link = linkColor ? linkColor.toString() : ''; if (this._helpLink) { this._helpLink.style.color = link; } } private registerListeners(): void { // Theme styler this._register(this._createButton!); this._register(this._closeButton!); // 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); })); // handler for to subnet ip range change events this._register(this._toRangeinputBox!.onDidChange(IPAddress => { this.toRangeInputChanged(IPAddress); })); } private firewallRuleNameChanged(ruleName: string) { this.viewModel.firewallRuleName = ruleName; } private fromRangeInputChanged(IPAddress: string) { this.viewModel.fromSubnetIPRange = IPAddress; } private toRangeInputChanged(IPAddress: string) { this.viewModel.toSubnetIPRange = IPAddress; } /* Overwrite escape key behavior */ protected override onClose() { this.cancel(); } /* Overwrite enter key behavior */ protected override onAccept() { this.createFirewallRule(); } public cancel() { this._onCancel.fire(); this.hide('cancel'); } public close() { this.hide('close'); } public createFirewallRule() { if (this._createButton!.enabled) { this._createButton!.enabled = false; this.spinner = true; this._onCreateFirewallRule.fire(); } } public setInitialAccountTenant(account: string, tenant: string) { this._accountPickerService.setInitialAccountTenant(account, tenant); } public onAccountSelectionChange(account: azdata.Account | undefined): void { this.viewModel.selectedAccount = account; if (account && !account.isStale) { this._createButton!.enabled = true; } else { this._createButton!.enabled = false; } } public onTenantSelectionChange(tenantId: string): void { this.viewModel.selectedTenantId = tenantId; } public onServiceComplete() { this._createButton!.enabled = true; this.spinner = false; } 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 ?? ''})`; this.show(); } }