move code from parts to contrib (#8319)
357
src/sql/workbench/contrib/accounts/browser/accountDialog.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/accountDialog';
|
||||
import 'vs/css!./media/accountActions';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { values } from 'vs/base/common/map';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { attachModalDialogStyler, attachButtonStyler, attachPanelStyler } from 'sql/platform/theme/common/styler';
|
||||
import { AccountViewModel } from 'sql/platform/accounts/common/accountViewModel';
|
||||
import { AddAccountAction } from 'sql/platform/accounts/common/accountActions';
|
||||
import { AccountListRenderer, AccountListDelegate } from 'sql/workbench/contrib/accounts/browser/accountListRenderer';
|
||||
import { AccountProviderAddedEventParams, UpdateAccountListEventParams } from 'sql/platform/accounts/common/eventTypes';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
class AccountPanel extends ViewletPanel {
|
||||
public index: number;
|
||||
private accountList: List<azdata.Account>;
|
||||
|
||||
constructor(
|
||||
private options: IViewletPanelOptions,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService);
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this.accountList = new List<azdata.Account>('AccountList', container, new AccountListDelegate(AccountDialog.ACCOUNTLIST_HEIGHT), [this.instantiationService.createInstance(AccountListRenderer)]);
|
||||
this._register(attachListStyler(this.accountList, this.themeService));
|
||||
}
|
||||
|
||||
protected layoutBody(size: number): void {
|
||||
if (this.accountList) {
|
||||
this.accountList.layout(size);
|
||||
}
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.accountList.length;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.accountList.domFocus();
|
||||
}
|
||||
|
||||
public updateAccounts(accounts: azdata.Account[]) {
|
||||
this.accountList.splice(0, this.accountList.length, accounts);
|
||||
}
|
||||
|
||||
public setSelection(indexes: number[]) {
|
||||
this.accountList.setSelection(indexes);
|
||||
}
|
||||
|
||||
public getActions(): IAction[] {
|
||||
return [this.instantiationService.createInstance(
|
||||
AddAccountAction,
|
||||
this.options.id
|
||||
)];
|
||||
}
|
||||
}
|
||||
|
||||
export interface IProviderViewUiComponent {
|
||||
view: AccountPanel;
|
||||
addAccountAction: AddAccountAction;
|
||||
}
|
||||
|
||||
export class AccountDialog extends Modal {
|
||||
public static ACCOUNTLIST_HEIGHT = 77;
|
||||
|
||||
public viewModel: AccountViewModel;
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _providerViewsMap = new Map<string, IProviderViewUiComponent>();
|
||||
|
||||
private _closeButton: Button;
|
||||
private _addAccountButton: Button;
|
||||
private _splitView: SplitView;
|
||||
private _container: HTMLElement;
|
||||
private _splitViewContainer: HTMLElement;
|
||||
private _noaccountViewContainer: HTMLElement;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _onAddAccountErrorEmitter: Emitter<string>;
|
||||
public get onAddAccountErrorEvent(): Event<string> { return this._onAddAccountErrorEmitter.event; }
|
||||
|
||||
private _onCloseEmitter: Emitter<void>;
|
||||
public get onCloseEvent(): Event<void> { return this._onCloseEmitter.event; }
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||
@IKeybindingService private _keybindingService: IKeybindingService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IClipboardService clipboardService: IClipboardService,
|
||||
@ILogService logService: ILogService,
|
||||
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
super(
|
||||
localize('linkedAccounts', "Linked accounts"),
|
||||
TelemetryKeys.Accounts,
|
||||
telemetryService,
|
||||
layoutService,
|
||||
clipboardService,
|
||||
themeService,
|
||||
logService,
|
||||
textResourcePropertiesService,
|
||||
contextKeyService,
|
||||
{ hasSpinner: true }
|
||||
);
|
||||
|
||||
// Setup the event emitters
|
||||
this._onAddAccountErrorEmitter = new Emitter<string>();
|
||||
this._onCloseEmitter = new Emitter<void>();
|
||||
|
||||
// Create the view model and wire up the events
|
||||
this.viewModel = this._instantiationService.createInstance(AccountViewModel);
|
||||
this.viewModel.addProviderEvent(arg => { this.addProvider(arg); });
|
||||
this.viewModel.removeProviderEvent(arg => { this.removeProvider(arg); });
|
||||
this.viewModel.updateAccountListEvent(arg => { this.updateProviderAccounts(arg); });
|
||||
|
||||
// Load the initial contents of the view model
|
||||
this.viewModel.initialize()
|
||||
.then(addedProviders => {
|
||||
for (const addedProvider of addedProviders) {
|
||||
this.addProvider(addedProvider);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// MODAL OVERRIDE METHODS //////////////////////////////////////////////
|
||||
protected layout(height?: number): void {
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
this._closeButton = this.addFooterButton(localize('accountDialog.close', "Close"), () => this.close());
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
this._container = container;
|
||||
this._splitViewContainer = DOM.$('div.account-view.monaco-panel-view');
|
||||
DOM.append(container, this._splitViewContainer);
|
||||
this._splitView = new SplitView(this._splitViewContainer);
|
||||
|
||||
this._noaccountViewContainer = DOM.$('div.no-account-view');
|
||||
const noAccountTitle = DOM.append(this._noaccountViewContainer, DOM.$('.no-account-view-label'));
|
||||
const noAccountLabel = localize('accountDialog.noAccountLabel', "There is no linked account. Please add an account.");
|
||||
noAccountTitle.innerText = noAccountLabel;
|
||||
|
||||
// Show the add account button for the first provider
|
||||
// Todo: If we have more than 1 provider, need to show all add account buttons for all providers
|
||||
const buttonSection = DOM.append(this._noaccountViewContainer, DOM.$('div.button-section'));
|
||||
this._addAccountButton = new Button(buttonSection);
|
||||
this._addAccountButton.label = localize('accountDialog.addConnection', "Add an account");
|
||||
this._register(this._addAccountButton.onDidClick(() => {
|
||||
(<IProviderViewUiComponent>values(this._providerViewsMap)[0]).addAccountAction.run();
|
||||
}));
|
||||
|
||||
DOM.append(container, this._noaccountViewContainer);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._addAccountButton, this._themeService));
|
||||
}
|
||||
|
||||
/* Overwrite escape key behavior */
|
||||
protected onClose() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this._onCloseEmitter.fire();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.show();
|
||||
if (!this.isEmptyLinkedAccount()) {
|
||||
this.showSplitView();
|
||||
} else {
|
||||
this.showNoAccountContainer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private showNoAccountContainer() {
|
||||
this._splitViewContainer.hidden = true;
|
||||
this._noaccountViewContainer.hidden = false;
|
||||
this._addAccountButton.focus();
|
||||
}
|
||||
|
||||
private showSplitView() {
|
||||
this._splitViewContainer.hidden = false;
|
||||
this._noaccountViewContainer.hidden = true;
|
||||
if (values(this._providerViewsMap).length > 0) {
|
||||
const firstView = values(this._providerViewsMap)[0];
|
||||
if (firstView instanceof AccountPanel) {
|
||||
firstView.setSelection([0]);
|
||||
firstView.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isEmptyLinkedAccount(): boolean {
|
||||
for (const provider of values(this._providerViewsMap)) {
|
||||
const listView = provider.view;
|
||||
if (listView && listView.length > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
for (const provider of values(this._providerViewsMap)) {
|
||||
if (provider.addAccountAction) {
|
||||
provider.addAccountAction.dispose();
|
||||
}
|
||||
if (provider.view) {
|
||||
provider.view.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private addProvider(newProvider: AccountProviderAddedEventParams) {
|
||||
|
||||
// Skip adding the provider if it already exists
|
||||
if (this._providerViewsMap.get(newProvider.addedProvider.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Account provider doesn't exist, so add it
|
||||
// Create a scoped add account action
|
||||
let addAccountAction = this._instantiationService.createInstance(
|
||||
AddAccountAction,
|
||||
newProvider.addedProvider.id
|
||||
);
|
||||
addAccountAction.addAccountCompleteEvent(() => this.spinner = false);
|
||||
addAccountAction.addAccountErrorEvent(msg => this._onAddAccountErrorEmitter.fire(msg));
|
||||
addAccountAction.addAccountStartEvent(() => this.spinner = true);
|
||||
|
||||
let providerView = new AccountPanel(
|
||||
{
|
||||
id: newProvider.addedProvider.id,
|
||||
title: newProvider.addedProvider.displayName,
|
||||
ariaHeaderLabel: newProvider.addedProvider.displayName
|
||||
},
|
||||
this._keybindingService,
|
||||
this._contextMenuService,
|
||||
this._configurationService,
|
||||
this._instantiationService,
|
||||
this._themeService,
|
||||
this.contextKeyService
|
||||
);
|
||||
|
||||
attachPanelStyler(providerView, this._themeService);
|
||||
|
||||
const insertIndex = this._splitView.length;
|
||||
// Append the list view to the split view
|
||||
this._splitView.addView(providerView, Sizing.Distribute, insertIndex);
|
||||
providerView.render();
|
||||
providerView.index = insertIndex;
|
||||
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
|
||||
// Set the initial items of the list
|
||||
providerView.updateAccounts(newProvider.initialAccounts);
|
||||
|
||||
if (newProvider.initialAccounts.length > 0 && this._splitViewContainer.hidden) {
|
||||
this.showSplitView();
|
||||
}
|
||||
|
||||
this.layout();
|
||||
|
||||
// Store the view for the provider and action
|
||||
this._providerViewsMap.set(newProvider.addedProvider.id, { view: providerView, addAccountAction: addAccountAction });
|
||||
}
|
||||
|
||||
private removeProvider(removedProvider: azdata.AccountProviderMetadata) {
|
||||
// Skip removing the provider if it doesn't exist
|
||||
const providerView = this._providerViewsMap.get(removedProvider.id);
|
||||
if (!providerView || !providerView.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the list view from the split view
|
||||
this._splitView.removeView(providerView.view.index);
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
|
||||
// Remove the list view from our internal map
|
||||
this._providerViewsMap.delete(removedProvider.id);
|
||||
this.layout();
|
||||
}
|
||||
|
||||
private updateProviderAccounts(args: UpdateAccountListEventParams) {
|
||||
const providerMapping = this._providerViewsMap.get(args.providerId);
|
||||
if (!providerMapping || !providerMapping.view) {
|
||||
return;
|
||||
}
|
||||
providerMapping.view.updateAccounts(args.accountList);
|
||||
|
||||
if (args.accountList.length > 0 && this._splitViewContainer.hidden) {
|
||||
this.showSplitView();
|
||||
}
|
||||
|
||||
if (this.isEmptyLinkedAccount() && this._noaccountViewContainer.hidden) {
|
||||
this.showNoAccountContainer();
|
||||
}
|
||||
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { AccountDialog } from 'sql/workbench/contrib/accounts/browser/accountDialog';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
|
||||
export class AccountDialogController {
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _addAccountErrorTitle = localize('accountDialog.addAccountErrorTitle', "Error adding account");
|
||||
|
||||
private _accountDialog: AccountDialog;
|
||||
public get accountDialog(): AccountDialog { return this._accountDialog; }
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Open account dialog
|
||||
*/
|
||||
public openAccountDialog(): void {
|
||||
// Create a new dialog if one doesn't exist
|
||||
if (!this._accountDialog) {
|
||||
this._accountDialog = this._instantiationService.createInstance(AccountDialog);
|
||||
this._accountDialog.onAddAccountErrorEvent(msg => this.handleOnAddAccountError(msg));
|
||||
this._accountDialog.onCloseEvent(() => this.handleOnClose());
|
||||
this._accountDialog.render();
|
||||
}
|
||||
|
||||
// Open the dialog
|
||||
this._accountDialog.open();
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private handleOnClose(): void { }
|
||||
|
||||
private handleOnAddAccountError(msg: string): void {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._addAccountErrorTitle, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/accountListRenderer';
|
||||
import 'vs/css!./media/accountActions';
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { ActionBar, IActionOptions } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { RemoveAccountAction, RefreshAccountAction } from 'sql/platform/accounts/common/accountActions';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
export class AccountListDelegate implements IListVirtualDelegate<azdata.Account> {
|
||||
|
||||
constructor(
|
||||
private _height: number
|
||||
) {
|
||||
}
|
||||
|
||||
public getHeight(element: azdata.Account): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public getTemplateId(element: azdata.Account): string {
|
||||
return 'accountListRenderer';
|
||||
}
|
||||
}
|
||||
|
||||
export interface AccountPickerListTemplate {
|
||||
root: HTMLElement;
|
||||
icon: HTMLElement;
|
||||
badgeContent: HTMLElement;
|
||||
contextualDisplayName: HTMLElement;
|
||||
label: HTMLElement;
|
||||
displayName: HTMLElement;
|
||||
}
|
||||
|
||||
export interface AccountListTemplate extends AccountPickerListTemplate {
|
||||
content: HTMLElement;
|
||||
actions: ActionBar;
|
||||
}
|
||||
|
||||
export class AccountPickerListRenderer implements IListRenderer<azdata.Account, AccountPickerListTemplate> {
|
||||
public static TEMPLATE_ID = 'accountListRenderer';
|
||||
|
||||
public get templateId(): string {
|
||||
return AccountPickerListRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public renderTemplate(container: HTMLElement): AccountPickerListTemplate {
|
||||
const tableTemplate: AccountPickerListTemplate = Object.create(null);
|
||||
const badge = DOM.$('div.badge');
|
||||
tableTemplate.root = DOM.append(container, DOM.$('div.list-row.account-picker-list'));
|
||||
tableTemplate.icon = DOM.append(tableTemplate.root, DOM.$('div.codicon'));
|
||||
DOM.append(tableTemplate.icon, badge);
|
||||
tableTemplate.badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
|
||||
tableTemplate.label = DOM.append(tableTemplate.root, DOM.$('div.label'));
|
||||
tableTemplate.contextualDisplayName = DOM.append(tableTemplate.label, DOM.$('div.contextual-display-name'));
|
||||
tableTemplate.displayName = DOM.append(tableTemplate.label, DOM.$('div.display-name'));
|
||||
return tableTemplate;
|
||||
}
|
||||
|
||||
public renderElement(account: azdata.Account, index: number, templateData: AccountPickerListTemplate): void {
|
||||
// Set the account icon
|
||||
templateData.icon.classList.add('account-logo', account.displayInfo.accountType);
|
||||
|
||||
templateData.contextualDisplayName.innerText = account.displayInfo.contextualDisplayName;
|
||||
|
||||
// show the account display name text, something like "User Name (user@email.com)"
|
||||
if (account.displayInfo.userId && account.displayInfo.displayName) {
|
||||
templateData.displayName.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.userId + ')';
|
||||
} else {
|
||||
templateData.displayName.innerText = account.displayInfo.displayName;
|
||||
}
|
||||
|
||||
if (account.isStale) {
|
||||
templateData.badgeContent.className = 'badge-content codicon warning-badge';
|
||||
} else {
|
||||
templateData.badgeContent.className = 'badge-content';
|
||||
}
|
||||
}
|
||||
|
||||
public disposeTemplate(template: AccountPickerListTemplate): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
public disposeElement(element: azdata.Account, index: number, templateData: AccountPickerListTemplate): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountListRenderer extends AccountPickerListRenderer {
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get templateId(): string {
|
||||
return AccountListRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public renderTemplate(container: HTMLElement): AccountListTemplate {
|
||||
const tableTemplate = super.renderTemplate(container) as AccountListTemplate;
|
||||
tableTemplate.content = DOM.append(tableTemplate.label, DOM.$('div.content'));
|
||||
tableTemplate.actions = new ActionBar(tableTemplate.root, { animated: false });
|
||||
|
||||
return tableTemplate;
|
||||
}
|
||||
|
||||
public renderElement(account: azdata.Account, index: number, templateData: AccountListTemplate): void {
|
||||
super.renderElement(account, index, templateData);
|
||||
if (account.isStale) {
|
||||
templateData.content.innerText = localize('refreshCredentials', "You need to refresh the credentials for this account.");
|
||||
} else {
|
||||
templateData.content.innerText = '';
|
||||
}
|
||||
templateData.actions.clear();
|
||||
|
||||
const actionOptions: IActionOptions = { icon: true, label: false };
|
||||
if (account.isStale) {
|
||||
const refreshAction = this._instantiationService.createInstance(RefreshAccountAction);
|
||||
refreshAction.account = account;
|
||||
templateData.actions.push(refreshAction, actionOptions);
|
||||
} else {
|
||||
// Todo: Will show filter action when API/GUI for filtering is implemented (#3022, #3024)
|
||||
// templateData.actions.push(new ApplyFilterAction(ApplyFilterAction.ID, ApplyFilterAction.LABEL), actionOptions);
|
||||
}
|
||||
|
||||
const removeAction = this._instantiationService.createInstance(RemoveAccountAction, account);
|
||||
templateData.actions.push(removeAction, actionOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { localize } from 'vs/nls';
|
||||
import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
|
||||
export interface IAccountContrib {
|
||||
id: string;
|
||||
icon?: IUserFriendlyIcon;
|
||||
}
|
||||
|
||||
export type IUserFriendlyIcon = string | { light: string; dark: string; };
|
||||
|
||||
const account: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: localize('carbon.extension.contributes.account.id', "Identifier of the account type"),
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
description: localize('carbon.extension.contributes.account.icon', "(Optional) Icon which is used to represent the accpunt in the UI. Either a file path or a themable configuration"),
|
||||
anyOf: [{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
description: localize('carbon.extension.contributes.account.icon.light', "Icon path when a light theme is used"),
|
||||
type: 'string'
|
||||
},
|
||||
dark: {
|
||||
description: localize('carbon.extension.contributes.account.icon.dark', "Icon path when a dark theme is used"),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const accountsContribution: IJSONSchema = {
|
||||
description: localize('carbon.extension.contributes.account', "Contributes icons to account provider."),
|
||||
oneOf: [
|
||||
account,
|
||||
{
|
||||
type: 'array',
|
||||
items: account
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IAccountContrib | IAccountContrib[]>({ extensionPoint: 'account-type', jsonSchema: accountsContribution }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(account: IAccountContrib, extension: IExtensionPointUser<any>) {
|
||||
const { icon, id } = account;
|
||||
if (icon) {
|
||||
const iconClass = id;
|
||||
if (typeof icon === 'string') {
|
||||
const path = resources.joinPath(extension.description.extensionLocation, icon);
|
||||
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(path)}`);
|
||||
} else {
|
||||
const light = resources.joinPath(extension.description.extensionLocation, icon.light);
|
||||
const dark = resources.joinPath(extension.description.extensionLocation, icon.dark);
|
||||
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(light)}`);
|
||||
createCSSRule(`.vs-dark .codicon.${iconClass}, .hc-black .codicon.${iconClass}`, `background-image: ${asCSSUrl(dark)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IAccountContrib>(value)) {
|
||||
for (let command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
20
src/sql/workbench/contrib/accounts/browser/accountPicker.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
export const IAccountPickerService = createDecorator<IAccountPickerService>('AccountPickerService');
|
||||
export interface IAccountPickerService {
|
||||
_serviceBrand: undefined;
|
||||
renderAccountPicker(container: HTMLElement): void;
|
||||
addAccountCompleteEvent: Event<void>;
|
||||
addAccountErrorEvent: Event<string>;
|
||||
addAccountStartEvent: Event<void>;
|
||||
onAccountSelectionChangeEvent: Event<azdata.Account | undefined>;
|
||||
selectedAccount: azdata.Account | undefined;
|
||||
}
|
||||
237
src/sql/workbench/contrib/accounts/browser/accountPickerImpl.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/accountPicker';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IDropdownOptions } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { IListEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { buttonBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { DropdownList } from 'sql/base/browser/ui/dropdownList/dropdownList';
|
||||
import { attachDropdownStyler } from 'sql/platform/theme/common/styler';
|
||||
import { AddAccountAction, RefreshAccountAction } from 'sql/platform/accounts/common/accountActions';
|
||||
import { AccountPickerListRenderer, AccountListDelegate } from 'sql/workbench/contrib/accounts/browser/accountListRenderer';
|
||||
import { AccountPickerViewModel } from 'sql/platform/accounts/common/accountPickerViewModel';
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
|
||||
export class AccountPicker extends Disposable {
|
||||
public static ACCOUNTPICKERLIST_HEIGHT = 47;
|
||||
public viewModel: AccountPickerViewModel;
|
||||
private _accountList: List<azdata.Account>;
|
||||
private _rootElement: HTMLElement;
|
||||
private _refreshContainer: HTMLElement;
|
||||
private _listContainer: HTMLElement;
|
||||
private _dropdown: DropdownList;
|
||||
private _refreshAccountAction: RefreshAccountAction;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _addAccountCompleteEmitter: Emitter<void>;
|
||||
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
|
||||
|
||||
private _addAccountErrorEmitter: Emitter<string>;
|
||||
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
|
||||
|
||||
private _addAccountStartEmitter: Emitter<void>;
|
||||
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
|
||||
|
||||
private _onAccountSelectionChangeEvent: Emitter<azdata.Account | undefined>;
|
||||
public get onAccountSelectionChangeEvent(): Event<azdata.Account | undefined> { return this._onAccountSelectionChangeEvent.event; }
|
||||
|
||||
constructor(
|
||||
private _providerId: string,
|
||||
@IThemeService private _themeService: IThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextViewService private _contextViewService: IContextViewService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Create event emitters
|
||||
this._addAccountCompleteEmitter = new Emitter<void>();
|
||||
this._addAccountErrorEmitter = new Emitter<string>();
|
||||
this._addAccountStartEmitter = new Emitter<void>();
|
||||
this._onAccountSelectionChangeEvent = new Emitter<azdata.Account>();
|
||||
|
||||
// Create the view model, wire up the events, and initialize with baseline data
|
||||
this.viewModel = this._instantiationService.createInstance(AccountPickerViewModel, this._providerId);
|
||||
this.viewModel.updateAccountListEvent(arg => {
|
||||
if (arg.providerId === this._providerId) {
|
||||
this.updateAccountList(arg.accountList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
/**
|
||||
* Render account picker
|
||||
*/
|
||||
public render(container: HTMLElement): void {
|
||||
DOM.append(container, this._rootElement);
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
/**
|
||||
* Create account picker component
|
||||
*/
|
||||
public createAccountPickerComponent() {
|
||||
// Create an account list
|
||||
const delegate = new AccountListDelegate(AccountPicker.ACCOUNTPICKERLIST_HEIGHT);
|
||||
const accountRenderer = new AccountPickerListRenderer();
|
||||
this._listContainer = DOM.$('div.account-list-container');
|
||||
this._accountList = new List<azdata.Account>('AccountPicker', this._listContainer, delegate, [accountRenderer]);
|
||||
this._register(attachListStyler(this._accountList, this._themeService));
|
||||
|
||||
this._rootElement = DOM.$('div.account-picker-container');
|
||||
|
||||
// Create a dropdown for account picker
|
||||
const option: IDropdownOptions = {
|
||||
contextViewProvider: this._contextViewService,
|
||||
labelRenderer: (container) => this.renderLabel(container)
|
||||
};
|
||||
|
||||
// Create the add account action
|
||||
const addAccountAction = this._instantiationService.createInstance(AddAccountAction, this._providerId);
|
||||
addAccountAction.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
|
||||
addAccountAction.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
|
||||
addAccountAction.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
|
||||
|
||||
this._dropdown = this._register(new DropdownList(this._rootElement, option, this._listContainer, this._accountList, addAccountAction));
|
||||
this._register(attachDropdownStyler(this._dropdown, this._themeService));
|
||||
this._register(this._accountList.onSelectionChange((e: IListEvent<azdata.Account>) => {
|
||||
if (e.elements.length === 1) {
|
||||
this._dropdown.renderLabel();
|
||||
this.onAccountSelectionChange(e.elements[0]);
|
||||
}
|
||||
}));
|
||||
|
||||
// Create refresh account action
|
||||
this._refreshContainer = DOM.append(this._rootElement, DOM.$('div.refresh-container'));
|
||||
DOM.append(this._refreshContainer, DOM.$('div.sql codicon warning'));
|
||||
const actionBar = new ActionBar(this._refreshContainer, { animated: false });
|
||||
this._refreshAccountAction = this._instantiationService.createInstance(RefreshAccountAction);
|
||||
actionBar.push(this._refreshAccountAction, { icon: false, label: true });
|
||||
|
||||
if (this._accountList.length > 0) {
|
||||
this._accountList.setSelection([0]);
|
||||
this.onAccountSelectionChange(this._accountList.getSelectedElements()[0]);
|
||||
} else {
|
||||
DOM.hide(this._refreshContainer);
|
||||
}
|
||||
|
||||
this._register(this._themeService.onThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this._themeService.getTheme());
|
||||
|
||||
// Load the initial contents of the view model
|
||||
this.viewModel.initialize()
|
||||
.then((accounts: azdata.Account[]) => {
|
||||
this.updateAccountList(accounts);
|
||||
});
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
super.dispose();
|
||||
if (this._accountList) {
|
||||
this._accountList.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private onAccountSelectionChange(account: azdata.Account | undefined) {
|
||||
this.viewModel.selectedAccount = account;
|
||||
if (account && account.isStale) {
|
||||
this._refreshAccountAction.account = account;
|
||||
DOM.show(this._refreshContainer);
|
||||
} else {
|
||||
DOM.hide(this._refreshContainer);
|
||||
}
|
||||
|
||||
this._onAccountSelectionChangeEvent.fire(account);
|
||||
}
|
||||
|
||||
private renderLabel(container: HTMLElement): IDisposable | null {
|
||||
if (container.hasChildNodes()) {
|
||||
for (let i = 0; i < container.childNodes.length; i++) {
|
||||
container.removeChild(container.childNodes.item(i));
|
||||
}
|
||||
}
|
||||
|
||||
const selectedAccounts = this._accountList.getSelectedElements();
|
||||
const account = selectedAccounts ? selectedAccounts[0] : undefined;
|
||||
if (account) {
|
||||
const badge = DOM.$('div.badge');
|
||||
const row = DOM.append(container, DOM.$('div.selected-account-container'));
|
||||
const icon = DOM.append(row, DOM.$('div.codicon'));
|
||||
DOM.append(icon, badge);
|
||||
const badgeContent = DOM.append(badge, DOM.$('div.badge-content'));
|
||||
const label = DOM.append(row, DOM.$('div.label'));
|
||||
|
||||
// Set the account icon
|
||||
icon.classList.add('icon', account.displayInfo.accountType);
|
||||
|
||||
// TODO: Pick between the light and dark logo
|
||||
label.innerText = account.displayInfo.displayName + ' (' + account.displayInfo.contextualDisplayName + ')';
|
||||
|
||||
if (account.isStale) {
|
||||
badgeContent.className = 'badge-content codicon warning-badge';
|
||||
} else {
|
||||
badgeContent.className = 'badge-content';
|
||||
}
|
||||
} else {
|
||||
const row = DOM.append(container, DOM.$('div.no-account-container'));
|
||||
row.innerText = AddAccountAction.LABEL + '...';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private updateAccountList(accounts: azdata.Account[]): void {
|
||||
// keep the selection to the current one
|
||||
const selectedElements = this._accountList.getSelectedElements();
|
||||
|
||||
// find selected index
|
||||
let selectedIndex: number | undefined;
|
||||
if (selectedElements.length > 0 && accounts.length > 0) {
|
||||
selectedIndex = firstIndex(accounts, (account) => {
|
||||
return (account.key.accountId === selectedElements[0].key.accountId);
|
||||
});
|
||||
}
|
||||
|
||||
// Replace the existing list with the new one
|
||||
this._accountList.splice(0, this._accountList.length, accounts);
|
||||
|
||||
if (this._accountList.length > 0) {
|
||||
if (selectedIndex && selectedIndex !== -1) {
|
||||
this._accountList.setSelection([selectedIndex]);
|
||||
} else {
|
||||
this._accountList.setSelection([0]);
|
||||
}
|
||||
} else {
|
||||
// if the account is empty, re-render dropdown label
|
||||
this.onAccountSelectionChange(undefined);
|
||||
this._dropdown.renderLabel();
|
||||
}
|
||||
|
||||
this._accountList.layout(this._accountList.contentHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update theming that is specific to account picker
|
||||
*/
|
||||
private updateTheme(theme: ITheme): void {
|
||||
const linkColor = theme.getColor(buttonBackground);
|
||||
const link = linkColor ? linkColor.toString() : null;
|
||||
this._refreshContainer.style.color = link;
|
||||
if (this._refreshContainer) {
|
||||
this._refreshContainer.style.color = link;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IAccountPickerService } from 'sql/workbench/contrib/accounts/browser/accountPicker';
|
||||
import { AccountPicker } from 'sql/workbench/contrib/accounts/browser/accountPickerImpl';
|
||||
|
||||
export class AccountPickerService implements IAccountPickerService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private _accountPicker: AccountPicker;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _addAccountCompleteEmitter: Emitter<void>;
|
||||
public get addAccountCompleteEvent(): Event<void> { return this._addAccountCompleteEmitter.event; }
|
||||
|
||||
private _addAccountErrorEmitter: Emitter<string>;
|
||||
public get addAccountErrorEvent(): Event<string> { return this._addAccountErrorEmitter.event; }
|
||||
|
||||
private _addAccountStartEmitter: Emitter<void>;
|
||||
public get addAccountStartEvent(): Event<void> { return this._addAccountStartEmitter.event; }
|
||||
|
||||
private _onAccountSelectionChangeEvent: Emitter<azdata.Account | undefined>;
|
||||
public get onAccountSelectionChangeEvent(): Event<azdata.Account | undefined> { return this._onAccountSelectionChangeEvent.event; }
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
// Create event emitters
|
||||
this._addAccountCompleteEmitter = new Emitter<void>();
|
||||
this._addAccountErrorEmitter = new Emitter<string>();
|
||||
this._addAccountStartEmitter = new Emitter<void>();
|
||||
this._onAccountSelectionChangeEvent = new Emitter<azdata.Account>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected account
|
||||
*/
|
||||
public get selectedAccount(): azdata.Account | undefined {
|
||||
return this._accountPicker.viewModel.selectedAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render account picker
|
||||
*/
|
||||
public renderAccountPicker(container: HTMLElement): void {
|
||||
if (!this._accountPicker) {
|
||||
// TODO: expand support to multiple providers
|
||||
const providerId: string = 'azurePublicCloud';
|
||||
this._accountPicker = this._instantiationService.createInstance(AccountPicker, providerId);
|
||||
this._accountPicker.createAccountPickerComponent();
|
||||
}
|
||||
|
||||
this._accountPicker.addAccountCompleteEvent(() => this._addAccountCompleteEmitter.fire());
|
||||
this._accountPicker.addAccountErrorEvent((msg) => this._addAccountErrorEmitter.fire(msg));
|
||||
this._accountPicker.addAccountStartEvent(() => this._addAccountStartEmitter.fire());
|
||||
this._accountPicker.onAccountSelectionChangeEvent((account) => this._onAccountSelectionChangeEvent.fire(account));
|
||||
this._accountPicker.render(container);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar';
|
||||
import { localize } from 'vs/nls';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
||||
|
||||
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
|
||||
CommandsRegistry.registerCommand('workbench.actions.modal.linkedAccount', accessor => {
|
||||
const accountManagementService = accessor.get(IAccountManagementService);
|
||||
accountManagementService.openAccountListDialog();
|
||||
});
|
||||
|
||||
class AccountsStatusBarContributions extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IStatusbarService private readonly statusbarService: IStatusbarService
|
||||
) {
|
||||
super();
|
||||
this._register(
|
||||
this.statusbarService.addEntry({
|
||||
command: 'workbench.actions.modal.linkedAccount',
|
||||
text: '$(person-filled)'
|
||||
},
|
||||
'status.accountList',
|
||||
localize('status.problems', "Problems"),
|
||||
StatusbarAlignment.LEFT, 15000 /* Highest Priority */)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
workbenchRegistry.registerWorkbenchContribution(AccountsStatusBarContributions, LifecyclePhase.Restored);
|
||||
157
src/sql/workbench/contrib/accounts/browser/autoOAuthDialog.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/autoOAuthDialog';
|
||||
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { $, append } from 'vs/base/browser/dom';
|
||||
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { attachModalDialogStyler, attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
export class AutoOAuthDialog extends Modal {
|
||||
private _copyAndOpenButton: Button;
|
||||
private _closeButton: Button;
|
||||
private _userCodeInputBox: InputBox;
|
||||
private _websiteInputBox: InputBox;
|
||||
private _descriptionElement: HTMLElement;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _onHandleAddAccount = new Emitter<void>();
|
||||
public get onHandleAddAccount(): Event<void> { return this._onHandleAddAccount.event; }
|
||||
|
||||
private _onCancel = new Emitter<void>();
|
||||
public get onCancel(): Event<void> { return this._onCancel.event; }
|
||||
|
||||
|
||||
private _onCloseEvent = new Emitter<void>();
|
||||
public get onCloseEvent(): Event<void> { return this._onCloseEvent.event; }
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IClipboardService clipboardService: IClipboardService,
|
||||
@ILogService logService: ILogService,
|
||||
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
super(
|
||||
'',
|
||||
TelemetryKeys.AutoOAuth,
|
||||
telemetryService,
|
||||
layoutService,
|
||||
clipboardService,
|
||||
themeService,
|
||||
logService,
|
||||
textResourcePropertiesService,
|
||||
contextKeyService,
|
||||
{
|
||||
isFlyout: true,
|
||||
hasBackButton: true,
|
||||
hasSpinner: true
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
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._copyAndOpenButton = this.addFooterButton(localize('copyAndOpen', "Copy & Open"), () => this.addAccount());
|
||||
this._closeButton = this.addFooterButton(localize('oauthDialog.cancel', "Cancel"), () => this.cancel());
|
||||
this.registerListeners();
|
||||
this._userCodeInputBox.disable();
|
||||
this._websiteInputBox.disable();
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
const body = append(container, $('.auto-oauth-dialog'));
|
||||
this._descriptionElement = append(body, $('.auto-oauth-description-section.new-section'));
|
||||
|
||||
const addAccountSection = append(body, $('.auto-oauth-info-section.new-section'));
|
||||
this._userCodeInputBox = this.createInputBoxHelper(addAccountSection, localize('userCode', "User code"));
|
||||
this._websiteInputBox = this.createInputBoxHelper(addAccountSection, localize('website', "Website"));
|
||||
}
|
||||
|
||||
private createInputBoxHelper(container: HTMLElement, label: string): InputBox {
|
||||
const inputContainer = append(container, $('.dialog-input-section'));
|
||||
append(inputContainer, $('.dialog-label')).innerText = label;
|
||||
const inputCellContainer = append(inputContainer, $('.dialog-input'));
|
||||
|
||||
return new InputBox(inputCellContainer, this._contextViewService, {
|
||||
ariaLabel: label
|
||||
});
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachButtonStyler(this._copyAndOpenButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
this._register(attachInputBoxStyler(this._userCodeInputBox, this._themeService));
|
||||
this._register(attachInputBoxStyler(this._websiteInputBox, this._themeService));
|
||||
|
||||
}
|
||||
|
||||
/* Overwrite escape key behavior */
|
||||
protected onClose() {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept() {
|
||||
this.addAccount();
|
||||
}
|
||||
|
||||
private addAccount() {
|
||||
if (this._copyAndOpenButton.enabled) {
|
||||
this._copyAndOpenButton.enabled = false;
|
||||
this.spinner = true;
|
||||
this._onHandleAddAccount.fire();
|
||||
}
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
this._onCancel.fire();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this._copyAndOpenButton.enabled = true;
|
||||
this._onCloseEvent.fire();
|
||||
this.spinner = false;
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public open(title: string, message: string, userCode: string, uri: string) {
|
||||
// Update dialog
|
||||
this.title = title;
|
||||
this._descriptionElement.innerText = message;
|
||||
this._userCodeInputBox.value = userCode;
|
||||
this._websiteInputBox.value = uri;
|
||||
this.show();
|
||||
this._copyAndOpenButton.focus();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { AutoOAuthDialog } from 'sql/workbench/contrib/accounts/browser/autoOAuthDialog';
|
||||
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
|
||||
export class AutoOAuthDialogController {
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _autoOAuthDialog: AutoOAuthDialog;
|
||||
private _providerId?: string;
|
||||
private _userCode: string;
|
||||
private _uri: string;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Open auto OAuth dialog
|
||||
*/
|
||||
public openAutoOAuthDialog(providerId: string, title: string, message: string, userCode: string, uri: string): Thenable<void> {
|
||||
if (this._providerId !== undefined) {
|
||||
// If a oauth flyout is already open, return an error
|
||||
const errorMessage = localize('oauthFlyoutIsAlreadyOpen', "Cannot start auto OAuth. An auto OAuth is already in progress.");
|
||||
this._errorMessageService.showDialog(Severity.Error, '', errorMessage);
|
||||
return Promise.reject(new Error('Auto OAuth dialog already open'));
|
||||
}
|
||||
|
||||
// Create a new dialog if one doesn't exist
|
||||
if (!this._autoOAuthDialog) {
|
||||
this._autoOAuthDialog = this._instantiationService.createInstance(AutoOAuthDialog);
|
||||
this._autoOAuthDialog.onHandleAddAccount(this.handleOnAddAccount, this);
|
||||
this._autoOAuthDialog.onCancel(this.handleOnCancel, this);
|
||||
this._autoOAuthDialog.onCloseEvent(this.handleOnClose, this);
|
||||
this._autoOAuthDialog.render();
|
||||
}
|
||||
|
||||
this._userCode = userCode;
|
||||
this._uri = uri;
|
||||
|
||||
// Open the dialog
|
||||
this._autoOAuthDialog.open(title, message, userCode, uri);
|
||||
this._providerId = providerId;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close auto OAuth dialog
|
||||
*/
|
||||
public closeAutoOAuthDialog(): void {
|
||||
this._autoOAuthDialog.close();
|
||||
this._providerId = undefined;
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private handleOnCancel(): void {
|
||||
this._accountManagementService.cancelAutoOAuthDeviceCode(this._providerId!); // this should be always true
|
||||
}
|
||||
|
||||
private handleOnClose(): void {
|
||||
this._providerId = undefined;
|
||||
}
|
||||
|
||||
private handleOnAddAccount(): void {
|
||||
this._accountManagementService.copyUserCodeAndOpenBrowser(this._userCode, this._uri);
|
||||
}
|
||||
}
|
||||
305
src/sql/workbench/contrib/accounts/browser/firewallRuleDialog.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IThemeService, ITheme } 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 { attachModalDialogStyler, attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { IAccountPickerService } from 'sql/workbench/contrib/accounts/browser/accountPicker';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
// 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")
|
||||
};
|
||||
|
||||
export class FirewallRuleDialog extends Modal {
|
||||
public viewModel: FirewallRuleViewModel;
|
||||
private _createButton: Button;
|
||||
private _closeButton: Button;
|
||||
private _fromRangeinputBox: InputBox;
|
||||
private _toRangeinputBox: InputBox;
|
||||
|
||||
private _helpLink: HTMLElement;
|
||||
private _IPAddressInput: HTMLElement;
|
||||
private _subnetIPRangeInput: HTMLElement;
|
||||
private _IPAddressElement: HTMLElement;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _onAddAccountErrorEmitter: Emitter<string>;
|
||||
public get onAddAccountErrorEvent(): Event<string> { return this._onAddAccountErrorEmitter.event; }
|
||||
|
||||
private _onCancel: Emitter<void>;
|
||||
public get onCancel(): Event<void> { return this._onCancel.event; }
|
||||
|
||||
private _onCreateFirewallRule: Emitter<void>;
|
||||
public get onCreateFirewallRule(): Event<void> { return this._onCreateFirewallRule.event; }
|
||||
|
||||
constructor(
|
||||
@IAccountPickerService private _accountPickerService: IAccountPickerService,
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@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(
|
||||
localize('createNewFirewallRule', "Create new firewall rule"),
|
||||
TelemetryKeys.FireWallRule,
|
||||
telemetryService,
|
||||
layoutService,
|
||||
clipboardService,
|
||||
themeService,
|
||||
logService,
|
||||
textResourcePropertiesService,
|
||||
contextKeyService,
|
||||
{
|
||||
isFlyout: true,
|
||||
hasBackButton: true,
|
||||
hasSpinner: true
|
||||
}
|
||||
);
|
||||
|
||||
// Setup event emitters
|
||||
this._onAddAccountErrorEmitter = new Emitter<string>();
|
||||
this._onCancel = new Emitter<void>();
|
||||
this._onCreateFirewallRule = new Emitter<void>();
|
||||
|
||||
this.viewModel = this._instantiationService.createInstance(FirewallRuleViewModel);
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
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());
|
||||
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'));
|
||||
|
||||
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._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.onclick = () => {
|
||||
this.openerService.open(URI.parse(firewallHelpUri));
|
||||
};
|
||||
|
||||
// Create account picker with event handling
|
||||
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));
|
||||
|
||||
const azureAccountSection = DOM.append(body, DOM.$('.azure-account-section.new-section'));
|
||||
const azureAccountLabel = localize('azureAccount', "Azure account");
|
||||
this.createLabelElement(azureAccountSection, azureAccountLabel, true);
|
||||
this._accountPickerService.renderAccountPicker(DOM.append(azureAccountSection, DOM.$('.dialog-input')));
|
||||
|
||||
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.onThemeChange(e => this.updateTheme(e)));
|
||||
this.updateTheme(this._themeService.getTheme());
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
DOM.append(container, DOM.$(`.${className}`)).innerText = content;
|
||||
}
|
||||
|
||||
// Update theming that is specific to firewall rule flyout body
|
||||
private updateTheme(theme: ITheme): void {
|
||||
const linkColor = theme.getColor(buttonBackground);
|
||||
const link = linkColor ? linkColor.toString() : null;
|
||||
if (this._helpLink) {
|
||||
this._helpLink.style.color = link;
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachButtonStyler(this._createButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
this._register(attachInputBoxStyler(this._fromRangeinputBox, this._themeService));
|
||||
this._register(attachInputBoxStyler(this._toRangeinputBox, this._themeService));
|
||||
|
||||
// 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 fromRangeInputChanged(IPAddress: string) {
|
||||
this.viewModel.fromSubnetIPRange = IPAddress;
|
||||
}
|
||||
|
||||
private toRangeInputChanged(IPAddress: string) {
|
||||
this.viewModel.toSubnetIPRange = IPAddress;
|
||||
}
|
||||
|
||||
/* Overwrite esapce key behavior */
|
||||
protected onClose() {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept() {
|
||||
this.createFirewallRule();
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
this._onCancel.fire();
|
||||
this.close();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public createFirewallRule() {
|
||||
if (this._createButton.enabled) {
|
||||
this._createButton.enabled = false;
|
||||
this.spinner = true;
|
||||
this._onCreateFirewallRule.fire();
|
||||
}
|
||||
}
|
||||
|
||||
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 onServiceComplete() {
|
||||
this._createButton.enabled = true;
|
||||
this.spinner = false;
|
||||
}
|
||||
|
||||
public open() {
|
||||
this._IPAddressInput.click();
|
||||
this.onAccountSelectionChange(this._accountPickerService.selectedAccount);
|
||||
this._fromRangeinputBox.setPlaceHolder(this.viewModel.defaultFromSubnetIPRange);
|
||||
this._toRangeinputBox.setPlaceHolder(this.viewModel.defaultToSubnetIPRange);
|
||||
this._IPAddressElement.innerText = '(' + this.viewModel.defaultIPAddress + ')';
|
||||
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { FirewallRuleDialog } from 'sql/workbench/contrib/accounts/browser/firewallRuleDialog';
|
||||
import { IAccountManagementService, AzureResource } from 'sql/platform/accounts/common/interfaces';
|
||||
import { IResourceProviderService } from 'sql/workbench/services/resourceProvider/common/resourceProviderService';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
|
||||
export class FirewallRuleDialogController {
|
||||
|
||||
private _firewallRuleDialog: FirewallRuleDialog;
|
||||
private _connection: IConnectionProfile;
|
||||
private _resourceProviderId: string;
|
||||
|
||||
private _addAccountErrorTitle = localize('firewallDialog.addAccountErrorTitle', "Error adding account");
|
||||
private _firewallRuleErrorTitle = localize('firewallRuleError', "Firewall rule error");
|
||||
private _deferredPromise: Deferred<boolean>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open firewall rule dialog
|
||||
*/
|
||||
public openFirewallRuleDialog(connection: IConnectionProfile, ipAddress: string, resourceProviderId: string): Promise<boolean> {
|
||||
if (!this._firewallRuleDialog) {
|
||||
this._firewallRuleDialog = this._instantiationService.createInstance(FirewallRuleDialog);
|
||||
this._firewallRuleDialog.onCancel(this.handleOnCancel, this);
|
||||
this._firewallRuleDialog.onCreateFirewallRule(this.handleOnCreateFirewallRule, this);
|
||||
this._firewallRuleDialog.onAddAccountErrorEvent(this.handleOnAddAccountError, this);
|
||||
this._firewallRuleDialog.render();
|
||||
}
|
||||
this._connection = connection;
|
||||
this._resourceProviderId = resourceProviderId;
|
||||
this._firewallRuleDialog.viewModel.updateDefaultValues(ipAddress);
|
||||
this._firewallRuleDialog.open();
|
||||
this._deferredPromise = new Deferred();
|
||||
return this._deferredPromise.promise;
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private handleOnAddAccountError(message: string): void {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._addAccountErrorTitle, message);
|
||||
}
|
||||
|
||||
private async handleOnCreateFirewallRule(): Promise<void> {
|
||||
const resourceProviderId = this._resourceProviderId;
|
||||
try {
|
||||
const securityTokenMappings = await this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount!, AzureResource.ResourceManagement);
|
||||
const firewallRuleInfo: azdata.FirewallRuleInfo = {
|
||||
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,
|
||||
securityTokenMappings
|
||||
};
|
||||
|
||||
const response = await this._resourceProviderService.createFirewallRule(this._firewallRuleDialog.viewModel.selectedAccount!, firewallRuleInfo, resourceProviderId);
|
||||
if (response.result) {
|
||||
this._firewallRuleDialog.close();
|
||||
this._deferredPromise.resolve(true);
|
||||
} else {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._firewallRuleErrorTitle, response.errorMessage);
|
||||
}
|
||||
this._firewallRuleDialog.onServiceComplete();
|
||||
} catch (e) {
|
||||
this.showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private showError(error: any): void {
|
||||
this._errorMessageService.showDialog(Severity.Error, this._firewallRuleErrorTitle, error);
|
||||
this._firewallRuleDialog.onServiceComplete();
|
||||
// Note: intentionally not rejecting the promise as we want users to be able to choose a different account
|
||||
}
|
||||
|
||||
private handleOnCancel(): void {
|
||||
this._deferredPromise.resolve(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Icons for various account actions */
|
||||
.vs .action-item .codicon.add-linked-account-action {
|
||||
background-image: url('new_account.svg');
|
||||
}
|
||||
|
||||
.vs-dark .action-item .codicon.add-linked-account-action,
|
||||
.hc-black .action-item .codicon.add-linked-account-action {
|
||||
background-image: url('new_account_inverse.svg');
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.no-account-view {
|
||||
font-size: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.no-account-view .no-account-view-label {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.account-view .header {
|
||||
position: relative;
|
||||
line-height: 22px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
padding-left: 20px;
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.account-view .header .title {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.account-view .header .actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.account-view .header .actions .action-item .action-label {
|
||||
width: 30px;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 0;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.account-view .header .count-badge-wrapper {
|
||||
justify-content: flex-end;
|
||||
/* hide the count badge as it is not providing much value and meanwhile not keyboard accessible*/
|
||||
display: none;
|
||||
}
|
||||
|
||||
.account-view .list-row {
|
||||
padding: 12px;
|
||||
line-height: 18px !important;
|
||||
}
|
||||
|
||||
.account-view .list-row .codicon {
|
||||
flex: 0 0 50px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-size: 50px;
|
||||
}
|
||||
|
||||
.account-view .list-row .codicon .badge {
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
left: 43px;
|
||||
overflow: hidden;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.account-view .list-row .codicon .badge .badge-content {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-size: 22px;
|
||||
}
|
||||
|
||||
.account-view .list-row .actions-container {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.account-view .list-row .actions-container .action-item .action-label {
|
||||
width: 16px;
|
||||
margin-left: 20px;
|
||||
margin-right: 10px;
|
||||
background-size: 16px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.account-view .list-row .actions-container .action-item .action-label.codicon.remove {
|
||||
background-size: 14px !important;
|
||||
}
|
||||
|
||||
.account-view .list-row .actions-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.account-view .monaco-list .monaco-list-row:hover .list-row .actions-container,
|
||||
.account-view .monaco-list .monaco-list-row.selected .list-row .actions-container,
|
||||
.account-view .monaco-list .monaco-list-row.focused .list-row .actions-container{
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.list-row.account-picker-list {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label .contextual-display-name {
|
||||
font-size: 15px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label .display-name {
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.list-row.account-picker-list .label .content {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.account-logo {
|
||||
background: no-repeat center center;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.statusbar-item .linked-account-staus a.linked-account-status-selection .linked-account-icon {
|
||||
-webkit-mask: url('accounts_statusbar_inverse.svg') no-repeat 50% 50%;
|
||||
-webkit-mask-size: 12px;
|
||||
width: 12px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.statusbar-item .linked-account-staus a.linked-account-status-selection {
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Selected account */
|
||||
.selected-account-container {
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.selected-account-container .codicon {
|
||||
flex: 0 0 25px;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.selected-account-container .label {
|
||||
flex: 1 1 auto;
|
||||
padding-left: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.selected-account-container .codicon {
|
||||
background-size: 25px;
|
||||
}
|
||||
|
||||
.selected-account-container .codicon .badge {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
overflow: hidden;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.selected-account-container .codicon .badge .badge-content {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-size: 12px;
|
||||
}
|
||||
|
||||
/* A container when the account list is empty */
|
||||
.no-account-container {
|
||||
padding: 6px;
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Account list */
|
||||
.account-list-container .list-row {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.account-list-container .list-row .codicon {
|
||||
flex: 0 0 35px;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
background-size: 35px;
|
||||
}
|
||||
|
||||
.account-list-container .list-row .codicon .badge {
|
||||
position: relative;
|
||||
top: 22px;
|
||||
left: 22px;
|
||||
overflow: hidden;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.account-list-container .list-row .codicon .badge .badge-content {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: 15px;
|
||||
}
|
||||
|
||||
/* Refresh link */
|
||||
.refresh-container {
|
||||
padding-left: 15px;
|
||||
padding-top: 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.refresh-container .monaco-action-bar .actions-container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.refresh-container .codicon {
|
||||
flex: 0 0 16px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.refresh-container .monaco-action-bar {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 10.89"><defs><style>.cls-1{fill:#fff;}</style></defs><title>accounts_statusbar_inverse_11x11</title><path class="cls-1" d="M10.08,5.66a3.44,3.44,0,0,0-.34-.75,3.47,3.47,0,0,0-.52-.64A3.23,3.23,0,0,0,8.8,4a2.28,2.28,0,0,0,.7-1.65,2.27,2.27,0,0,0-.18-.89A2.31,2.31,0,0,0,8.09.19,2.19,2.19,0,0,0,7.19,0,2.16,2.16,0,0,0,6.3.19a2.41,2.41,0,0,0-.73.49,2.41,2.41,0,0,0-.49.73,2.16,2.16,0,0,0-.19.89,2.22,2.22,0,0,0,.25,1A2.31,2.31,0,0,0,5.58,4a3.62,3.62,0,0,0-.52.41,3.62,3.62,0,0,0-.41.52A2.31,2.31,0,0,0,4,4.44a2.26,2.26,0,0,0-1-.25,2.16,2.16,0,0,0-.89.19,2.41,2.41,0,0,0-.73.49,2.41,2.41,0,0,0-.49.73,2.16,2.16,0,0,0-.19.89,2.22,2.22,0,0,0,.25,1,2.31,2.31,0,0,0,.45.61A3.7,3.7,0,0,0,1,8.47a3.47,3.47,0,0,0-.52.64,2.88,2.88,0,0,0-.34.75,3.06,3.06,0,0,0-.12.83v.21H1.12v-.21A1.83,1.83,0,0,1,1.27,10a1.9,1.9,0,0,1,1-1A1.83,1.83,0,0,1,3,8.8a1.86,1.86,0,0,1,.73.15,2,2,0,0,1,.6.4,1.9,1.9,0,0,1,.4.6,1.85,1.85,0,0,1,.15.73v.21H6v-.21a3.05,3.05,0,0,0-.11-.83,3.44,3.44,0,0,0-.34-.75A3.25,3.25,0,0,0,5,8.47a3.23,3.23,0,0,0-.43-.32,2.28,2.28,0,0,0,.7-1.65,1.83,1.83,0,0,1,.15-.73,1.9,1.9,0,0,1,1-1,1.83,1.83,0,0,1,.73-.15,1.86,1.86,0,0,1,.73.15,1.9,1.9,0,0,1,1,1,1.82,1.82,0,0,1,.15.73V6.7h1.12V6.49A3.05,3.05,0,0,0,10.08,5.66ZM4.1,7a1.24,1.24,0,0,1-.64.64A1.13,1.13,0,0,1,3,7.68a1.1,1.1,0,0,1-.45-.09,1.21,1.21,0,0,1-.38-.26A1.1,1.1,0,0,1,1.91,7a1.11,1.11,0,0,1-.1-.46A1.13,1.13,0,0,1,1.91,6a1.1,1.1,0,0,1,.26-.38,1.1,1.1,0,0,1,.38-.26A1.1,1.1,0,0,1,3,5.31a1.16,1.16,0,0,1,.46.1A1.24,1.24,0,0,1,4.1,6a1.15,1.15,0,0,1,.09.45A1.13,1.13,0,0,1,4.1,7ZM8.29,2.76a1.24,1.24,0,0,1-.64.64,1.13,1.13,0,0,1-.46.09,1.1,1.1,0,0,1-.45-.09,1.21,1.21,0,0,1-.38-.26,1.1,1.1,0,0,1-.26-.38A1.14,1.14,0,0,1,6,2.3a1.15,1.15,0,0,1,.1-.45,1.1,1.1,0,0,1,.26-.38,1.19,1.19,0,0,1,.83-.36,1.16,1.16,0,0,1,.46.1A1.1,1.1,0,0,1,8,1.47a1.21,1.21,0,0,1,.26.38,1.15,1.15,0,0,1,.09.45A1.13,1.13,0,0,1,8.29,2.76Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.auto-oauth-dialog {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.modal .auto-oauth-dialog .new-section {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.modal .auto-oauth-dialog .dialog-input-section {
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal .auto-oauth-dialog .dialog-input-section .dialog-label {
|
||||
flex: 0 0 100px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.modal .auto-oauth-dialog .dialog-input-section .dialog-input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.firewall-rule-dialog {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-label.header {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .new-section {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input-section {
|
||||
display: flex;
|
||||
padding-left: 30px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input-section .dialog-label {
|
||||
flex: 0 0 15px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .dialog-input-section .dialog-input {
|
||||
flex: 0 0 100px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .azure-account-section {
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
/* Firewall rule description section */
|
||||
.modal .firewall-rule-dialog a:link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-description-section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-description-section .firewall-rule-icon {
|
||||
background: url('secure.svg') center center no-repeat;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-description-section .firewall-rule-description {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
/* Radio button section*/
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-input {
|
||||
margin-top: 0px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-description {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.modal .firewall-rule-dialog .firewall-rule-section .radio-section .option-container .option-ip-address {
|
||||
padding-left: 3px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>new_account_16x16</title><path d="M8.09,7.39a5.91,5.91,0,0,1,1.38.74,6.06,6.06,0,0,1,1.13,1.06A6,6,0,0,1,11.88,12h-1a4.94,4.94,0,0,0-.63-1.61A5,5,0,0,0,7.66,8.3a5,5,0,0,0-3-.12,5.09,5.09,0,0,0-1.2.5,5,5,0,0,0-1.79,1.79,5.07,5.07,0,0,0-.5,1.2A4.88,4.88,0,0,0,1,13H0a5.88,5.88,0,0,1,.28-1.8A6,6,0,0,1,1,9.59a6.1,6.1,0,0,1,1.22-1.3,5.8,5.8,0,0,1,1.59-.9A4.2,4.2,0,0,1,2.46,5.94,3.86,3.86,0,0,1,2,4a3.92,3.92,0,0,1,.31-1.56A4,4,0,0,1,4.4.31a4,4,0,0,1,3.12,0A4,4,0,0,1,9.65,2.44,4,4,0,0,1,9.83,5a4,4,0,0,1-1,1.74A3.94,3.94,0,0,1,8.09,7.39ZM3,4A2.92,2.92,0,0,0,3.2,5.17a3,3,0,0,0,1.6,1.6,3,3,0,0,0,2.33,0,3,3,0,0,0,1.6-1.6,3,3,0,0,0,0-2.33,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.92,2.92,0,0,0,3,4Zm13,9v1H14v2H13V14H11V13h2V11h1v2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 850 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>new_account_inverse_16x16</title><path class="cls-1" d="M8.13,7.39a5.91,5.91,0,0,1,1.38.74,6.06,6.06,0,0,1,1.13,1.06A6,6,0,0,1,11.91,12h-1a4.94,4.94,0,0,0-.63-1.61A5,5,0,0,0,7.7,8.3a5,5,0,0,0-3-.12,5.09,5.09,0,0,0-1.2.5,5,5,0,0,0-1.79,1.79,5.07,5.07,0,0,0-.5,1.2A4.88,4.88,0,0,0,1,13H0a5.88,5.88,0,0,1,.28-1.8,6,6,0,0,1,.79-1.6,6.1,6.1,0,0,1,1.22-1.3,5.8,5.8,0,0,1,1.59-.9A4.2,4.2,0,0,1,2.5,5.94,3.86,3.86,0,0,1,2,4a3.92,3.92,0,0,1,.31-1.56A4,4,0,0,1,4.44.31a4,4,0,0,1,3.12,0A4,4,0,0,1,9.69,2.44,4,4,0,0,1,9.87,5a4,4,0,0,1-1,1.74A3.94,3.94,0,0,1,8.13,7.39ZM3,4a2.92,2.92,0,0,0,.23,1.17,3,3,0,0,0,1.6,1.6,3,3,0,0,0,2.33,0,3,3,0,0,0,1.6-1.6,3,3,0,0,0,0-2.33,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.92,2.92,0,0,0,3,4Zm13,9v1H14v2H13V14H11V13h2V11h1v2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 918 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36.65 50"><defs><style>.cls-1{fill:#7fba00;}.cls-2{fill:#b8d432;opacity:0.4;isolation:isolate;}.cls-3{fill:#fff;}</style></defs><title>secure</title><path class="cls-1" d="M31.19,6h0c-11-1.22-12.86-6-12.86-6S15.85,6.24,0,6.24V31.85c0,3.1,1.72,6,4.1,8.53h0C9.51,46.13,18.33,50,18.33,50s18.33-8,18.33-18.15V6.24A49.77,49.77,0,0,1,31.19,6Z"/><path class="cls-2" d="M22.86,16.54,31.19,6c-11-1.22-12.86-6-12.86-6S15.85,6.24,0,6.24V31.85c0,3.1,1.72,6,4.1,8.53l6.15-7.82Z"/><path class="cls-3" d="M25.6,24.46H24.53V20.91a6.7,6.7,0,0,0-1.67-4.45h0l-.11-.13a6,6,0,0,0-8.84,0,6.7,6.7,0,0,0-1.78,4.58v3.55H11.06a.8.8,0,0,0-.8.8v7.22h0v2.17a.8.8,0,0,0,.8.8H25.6a.8.8,0,0,0,.8-.8V25.26A.8.8,0,0,0,25.6,24.46Zm-4,0H15.07V20.91a3.75,3.75,0,0,1,1-2.57,3,3,0,0,1,4.54,0,3.67,3.67,0,0,1,.39.5h0a3.8,3.8,0,0,1,.6,2.06Z"/></svg>
|
||||
|
After Width: | Height: | Size: 894 B |
@@ -0,0 +1,103 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { AccountDialog } from 'sql/workbench/contrib/accounts/browser/accountDialog';
|
||||
import { AccountDialogController } from 'sql/workbench/contrib/accounts/browser/accountDialogController';
|
||||
import { AccountViewModel } from 'sql/platform/accounts/common/accountViewModel';
|
||||
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
|
||||
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
import { AccountListRenderer } from 'sql/workbench/contrib/accounts/browser/accountListRenderer';
|
||||
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
|
||||
// TESTS ///////////////////////////////////////////////////////////////////
|
||||
suite('Account Management Dialog Controller Tests', () => {
|
||||
test('Open Account Dialog - Dialog Doesn\'t Exist', () => {
|
||||
// Setup: Create instance of the controller
|
||||
let instantiationService = createInstantiationService();
|
||||
let controller = new AccountDialogController(instantiationService, undefined!);
|
||||
assert.strictEqual(controller.accountDialog, undefined);
|
||||
|
||||
// If: I open the account dialog when one hasn't been opened
|
||||
controller.openAccountDialog();
|
||||
|
||||
// Then:
|
||||
// ... The account dialog should be defined
|
||||
assert.notStrictEqual(controller.accountDialog, undefined);
|
||||
});
|
||||
|
||||
test('Open Account Dialog - Dialog Exists', () => {
|
||||
// Setup: Create instance of the controller with an account dialog already loaded
|
||||
let instantiationService = createInstantiationService();
|
||||
let controller = new AccountDialogController(instantiationService, undefined!);
|
||||
controller.openAccountDialog();
|
||||
let accountDialog = controller.accountDialog;
|
||||
|
||||
// If: I open the account dialog when one has already been opened
|
||||
controller.openAccountDialog();
|
||||
|
||||
// Then: It should be the same dialog that already existed
|
||||
assert.equal(controller.accountDialog, accountDialog);
|
||||
});
|
||||
|
||||
test('Add Account Failure - Error Message Shown', () => {
|
||||
// Setup:
|
||||
// ... Create instantiation service that returns mock emitter for account dialog
|
||||
let mockEventEmitter = new Emitter<string>();
|
||||
let instantiationService = createInstantiationService(mockEventEmitter);
|
||||
|
||||
// ... Create a mock instance of the error message service
|
||||
let errorMessageServiceStub = new TestErrorMessageService();
|
||||
let mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
|
||||
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
|
||||
// ... Create instance of the controller with an opened dialog
|
||||
let controller = new AccountDialogController(instantiationService, mockErrorMessageService.object);
|
||||
controller.openAccountDialog();
|
||||
|
||||
// If: The account dialog reports a failure adding an account
|
||||
mockEventEmitter.fire('Error message');
|
||||
|
||||
// Then: An error dialog should have been opened
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
});
|
||||
|
||||
function createInstantiationService(addAccountFailureEmitter?: Emitter<string>): InstantiationService {
|
||||
// Create a mock account dialog view model
|
||||
let accountViewModel = new AccountViewModel(new TestAccountManagementService());
|
||||
let mockAccountViewModel = TypeMoq.Mock.ofInstance(accountViewModel);
|
||||
let mockEvent = new Emitter<any>();
|
||||
mockAccountViewModel.setup(x => x.addProviderEvent).returns(() => mockEvent.event);
|
||||
mockAccountViewModel.setup(x => x.removeProviderEvent).returns(() => mockEvent.event);
|
||||
mockAccountViewModel.setup(x => x.updateAccountListEvent).returns(() => mockEvent.event);
|
||||
mockAccountViewModel.setup(x => x.initialize()).returns(() => Promise.resolve([]));
|
||||
|
||||
// Create a mocked out instantiation service
|
||||
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountViewModel)))
|
||||
.returns(() => mockAccountViewModel.object);
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountListRenderer)))
|
||||
.returns(() => undefined);
|
||||
|
||||
// Create a mock account dialog
|
||||
let accountDialog = new AccountDialog(undefined!, undefined!, instantiationService.object, undefined!, undefined!, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!);
|
||||
let mockAccountDialog = TypeMoq.Mock.ofInstance(accountDialog);
|
||||
mockAccountDialog.setup(x => x.onAddAccountErrorEvent)
|
||||
.returns(() => { return addAccountFailureEmitter ? addAccountFailureEmitter.event : mockEvent.event; });
|
||||
mockAccountDialog.setup(x => x.onCloseEvent)
|
||||
.returns(() => mockEvent.event);
|
||||
mockAccountDialog.setup(x => x.render())
|
||||
.returns(() => undefined);
|
||||
mockAccountDialog.setup(x => x.open())
|
||||
.returns(() => undefined);
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountDialog)))
|
||||
.returns(() => mockAccountDialog.object);
|
||||
|
||||
return instantiationService.object;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as assert from 'assert';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { EventVerifierSingle } from 'sql/base/test/common/event';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { AccountPicker } from 'sql/workbench/contrib/accounts/browser/accountPickerImpl';
|
||||
import { AccountPickerService } from 'sql/workbench/contrib/accounts/browser/accountPickerService';
|
||||
import { AccountPickerViewModel } from 'sql/platform/accounts/common/accountPickerViewModel';
|
||||
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
|
||||
// SUITE STATE /////////////////////////////////////////////////////////////
|
||||
let mockAddAccountCompleteEmitter: Emitter<void>;
|
||||
let mockAddAccountErrorEmitter: Emitter<string>;
|
||||
let mockAddAccountStartEmitter: Emitter<void>;
|
||||
let mockOnAccountSelectionChangeEvent: Emitter<azdata.Account>;
|
||||
|
||||
// TESTS ///////////////////////////////////////////////////////////////////
|
||||
suite('Account picker service tests', () => {
|
||||
setup(() => {
|
||||
// Setup event mocks for the account picker service
|
||||
mockAddAccountCompleteEmitter = new Emitter<void>();
|
||||
mockAddAccountErrorEmitter = new Emitter<string>();
|
||||
mockAddAccountStartEmitter = new Emitter<void>();
|
||||
mockOnAccountSelectionChangeEvent = new Emitter<azdata.Account>();
|
||||
});
|
||||
|
||||
test('Construction - Events are properly defined', () => {
|
||||
// Setup:
|
||||
// ... Create instantiation service
|
||||
let instantiationService = createInstantiationService();
|
||||
|
||||
// ... Create instance of the service and reder account picker
|
||||
let service = new AccountPickerService(instantiationService);
|
||||
service.renderAccountPicker(TypeMoq.It.isAny());
|
||||
|
||||
// Then:
|
||||
// ... All the events for the view models should be properly initialized
|
||||
assert.notEqual(service.addAccountCompleteEvent, undefined);
|
||||
assert.notEqual(service.addAccountErrorEvent, undefined);
|
||||
assert.notEqual(service.addAccountStartEvent, undefined);
|
||||
assert.notEqual(service.onAccountSelectionChangeEvent, undefined);
|
||||
|
||||
|
||||
// ... All the events should properly fire
|
||||
let evAddAccountCompleteEvent = new EventVerifierSingle<void>();
|
||||
service.addAccountCompleteEvent(evAddAccountCompleteEvent.eventHandler);
|
||||
mockAddAccountCompleteEmitter.fire();
|
||||
evAddAccountCompleteEvent.assertFired();
|
||||
|
||||
let errorMsg = 'Error';
|
||||
let evAddAccountErrorEvent = new EventVerifierSingle<string>();
|
||||
service.addAccountErrorEvent(evAddAccountErrorEvent.eventHandler);
|
||||
mockAddAccountErrorEmitter.fire(errorMsg);
|
||||
evAddAccountErrorEvent.assertFired(errorMsg);
|
||||
|
||||
let evAddAccountStartEvent = new EventVerifierSingle<void>();
|
||||
service.addAccountStartEvent(evAddAccountStartEvent.eventHandler);
|
||||
mockAddAccountStartEmitter.fire();
|
||||
evAddAccountStartEvent.assertFired();
|
||||
|
||||
let account = {
|
||||
key: { providerId: 'azure', accountId: 'account1' },
|
||||
name: 'Account 1',
|
||||
displayInfo: {
|
||||
contextualDisplayName: 'Microsoft Account',
|
||||
accountType: 'microsoft',
|
||||
displayName: 'Account 1',
|
||||
userId: 'user@email.com'
|
||||
},
|
||||
properties: [],
|
||||
isStale: false
|
||||
};
|
||||
let evOnAccountSelectionChangeEvent = new EventVerifierSingle<azdata.Account>();
|
||||
service.onAccountSelectionChangeEvent(evOnAccountSelectionChangeEvent.eventHandler);
|
||||
mockOnAccountSelectionChangeEvent.fire(account);
|
||||
evOnAccountSelectionChangeEvent.assertFired(account);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function createInstantiationService(): InstantiationService {
|
||||
// Create a mock account picker view model
|
||||
let providerId = 'azure';
|
||||
let accountPickerViewModel = new AccountPickerViewModel(providerId, new TestAccountManagementService());
|
||||
let mockAccountViewModel = TypeMoq.Mock.ofInstance(accountPickerViewModel);
|
||||
let mockEvent = new Emitter<any>();
|
||||
mockAccountViewModel.setup(x => x.updateAccountListEvent).returns(() => mockEvent.event);
|
||||
|
||||
// Create a mocked out instantiation service
|
||||
let instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountPickerViewModel), TypeMoq.It.isAny()))
|
||||
.returns(() => mockAccountViewModel.object);
|
||||
|
||||
// Create a mock account picker
|
||||
|
||||
let accountPicker = new AccountPicker('provider', new TestThemeService(), instantiationService.object, undefined!);
|
||||
let mockAccountDialog = TypeMoq.Mock.ofInstance(accountPicker);
|
||||
|
||||
mockAccountDialog.setup(x => x.addAccountCompleteEvent)
|
||||
.returns(() => mockAddAccountCompleteEmitter.event);
|
||||
mockAccountDialog.setup(x => x.addAccountErrorEvent)
|
||||
.returns((msg) => mockAddAccountErrorEmitter.event);
|
||||
mockAccountDialog.setup(x => x.addAccountStartEvent)
|
||||
.returns(() => mockAddAccountStartEmitter.event);
|
||||
mockAccountDialog.setup(x => x.onAccountSelectionChangeEvent)
|
||||
.returns((account) => mockOnAccountSelectionChangeEvent.event);
|
||||
mockAccountDialog.setup(x => x.render(TypeMoq.It.isAny()))
|
||||
.returns((container) => undefined);
|
||||
mockAccountDialog.setup(x => x.createAccountPickerComponent());
|
||||
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountPicker), TypeMoq.It.isAny()))
|
||||
.returns(() => mockAccountDialog.object);
|
||||
|
||||
return instantiationService.object;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
|
||||
import { AutoOAuthDialog } from 'sql/workbench/contrib/accounts/browser/autoOAuthDialog';
|
||||
import { AutoOAuthDialogController } from 'sql/workbench/contrib/accounts/browser/autoOAuthDialogController';
|
||||
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
|
||||
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
|
||||
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
|
||||
// TESTS ///////////////////////////////////////////////////////////////////
|
||||
suite('auto OAuth dialog controller tests', () => {
|
||||
let instantiationService: TypeMoq.Mock<InstantiationService>;
|
||||
let mockAutoOAuthDialog: TypeMoq.Mock<AutoOAuthDialog>;
|
||||
let mockAccountManagementService: TypeMoq.Mock<TestAccountManagementService>;
|
||||
let mockErrorMessageService: TypeMoq.Mock<TestErrorMessageService>;
|
||||
let autoOAuthDialogController: AutoOAuthDialogController;
|
||||
|
||||
let mockOnCancelEvent: Emitter<void>;
|
||||
let mockOnAddAccountEvent: Emitter<void>;
|
||||
let mockOnCloseEvent: Emitter<void>;
|
||||
|
||||
let providerId = 'azure';
|
||||
let title = 'Add Account';
|
||||
let message = 'This is the dialog description';
|
||||
let userCode = 'abcde';
|
||||
let uri = 'uri';
|
||||
|
||||
setup(() => {
|
||||
mockOnCancelEvent = new Emitter<void>();
|
||||
mockOnAddAccountEvent = new Emitter<void>();
|
||||
mockOnCloseEvent = new Emitter<void>();
|
||||
|
||||
// Create a mock auto OAuth dialog
|
||||
let autoOAuthDialog = new AutoOAuthDialog(undefined!, undefined!, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!);
|
||||
mockAutoOAuthDialog = TypeMoq.Mock.ofInstance(autoOAuthDialog);
|
||||
|
||||
mockAutoOAuthDialog.setup(x => x.onCancel).returns(() => mockOnCancelEvent.event);
|
||||
mockAutoOAuthDialog.setup(x => x.onHandleAddAccount).returns(() => mockOnAddAccountEvent.event);
|
||||
mockAutoOAuthDialog.setup(x => x.onCloseEvent).returns(() => mockOnCloseEvent.event);
|
||||
mockAutoOAuthDialog.setup(x => x.render());
|
||||
mockAutoOAuthDialog.setup(x => x.open(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
mockAutoOAuthDialog.setup(x => x.close()).callback(() => {
|
||||
mockOnCloseEvent.fire();
|
||||
});
|
||||
|
||||
// Create a mocked out instantiation service
|
||||
instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AutoOAuthDialog)))
|
||||
.returns(() => mockAutoOAuthDialog.object);
|
||||
|
||||
|
||||
// Create a mocked account management service
|
||||
let accountManagementTestService = new TestAccountManagementService();
|
||||
mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
|
||||
mockAccountManagementService.setup(x => x.copyUserCodeAndOpenBrowser(TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
|
||||
// Create a mocked error message service
|
||||
let errorMessageServiceStub = new TestErrorMessageService();
|
||||
mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
|
||||
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
|
||||
// Create a mocked auto OAuth dialog controller
|
||||
autoOAuthDialogController = new AutoOAuthDialogController(instantiationService.object, mockAccountManagementService.object, mockErrorMessageService.object);
|
||||
|
||||
});
|
||||
|
||||
test('Open auto OAuth when the flyout is already open, return an error', (done) => {
|
||||
|
||||
// If: Open auto OAuth dialog first time
|
||||
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
|
||||
|
||||
// Then: It should open the flyout successfully
|
||||
mockAutoOAuthDialog.verify(x => x.open(title, message, userCode, uri), TypeMoq.Times.once());
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||
|
||||
// If: a oauth flyout is already open
|
||||
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri)
|
||||
.then(success => done('Failure: Expected error on 2nd dialog open'), error => done());
|
||||
|
||||
// Then: An error dialog should have been opened
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
test('Close auto OAuth dialog successfully', () => {
|
||||
let title = 'Add Account';
|
||||
let message = 'This is the dialog description';
|
||||
let userCode = 'abcde';
|
||||
let uri = 'uri';
|
||||
|
||||
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
|
||||
|
||||
// If: closeAutoOAuthDialog is called
|
||||
autoOAuthDialogController.closeAutoOAuthDialog();
|
||||
|
||||
// Then: it should close the dialog
|
||||
mockAutoOAuthDialog.verify(x => x.close(), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
test('Open and close auto OAuth dialog multiple times should work properly', () => {
|
||||
let title = 'Add Account';
|
||||
let message = 'This is the dialog description';
|
||||
let userCode = 'abcde';
|
||||
let uri = 'uri';
|
||||
|
||||
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
|
||||
autoOAuthDialogController.closeAutoOAuthDialog();
|
||||
|
||||
// If: Open the flyout second time
|
||||
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
|
||||
|
||||
// Then: It should open the flyout twice successfully
|
||||
mockAutoOAuthDialog.verify(x => x.open(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(2));
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||
});
|
||||
|
||||
test('Copy and open button in auto OAuth dialog should work properly', () => {
|
||||
let title = 'Add Account';
|
||||
let message = 'This is the dialog description';
|
||||
let userCode = 'abcde';
|
||||
let uri = 'uri';
|
||||
|
||||
autoOAuthDialogController.openAutoOAuthDialog(providerId, title, message, userCode, uri);
|
||||
|
||||
// If: the 'copy & open' button in auto Oauth dialog is selected
|
||||
mockOnAddAccountEvent.fire();
|
||||
|
||||
// Then: copyUserCodeAndOpenBrowser should get called
|
||||
mockAccountManagementService.verify(x => x.copyUserCodeAndOpenBrowser(userCode, uri), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
// TODO: Test for cancel button
|
||||
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { FirewallRuleDialog } from 'sql/workbench/contrib/accounts/browser/firewallRuleDialog';
|
||||
import { FirewallRuleViewModel } from 'sql/platform/accounts/common/firewallRuleViewModel';
|
||||
import { FirewallRuleDialogController } from 'sql/workbench/contrib/accounts/browser/firewallRuleDialogController';
|
||||
import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService';
|
||||
import { TestResourceProvider } from 'sql/workbench/services/resourceProvider/test/common/testResourceProviderService';
|
||||
import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService';
|
||||
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
|
||||
// TESTS ///////////////////////////////////////////////////////////////////
|
||||
suite('Firewall rule dialog controller tests', () => {
|
||||
let connectionProfile: IConnectionProfile;
|
||||
let account: azdata.Account;
|
||||
let IPAddress = '250.222.155.198';
|
||||
let mockOnAddAccountErrorEvent: Emitter<string>;
|
||||
let mockOnCreateFirewallRule: Emitter<void>;
|
||||
|
||||
let instantiationService: TypeMoq.Mock<InstantiationService>;
|
||||
let mockFirewallRuleViewModel: TypeMoq.Mock<FirewallRuleViewModel>;
|
||||
let mockFirewallRuleDialog: TypeMoq.Mock<FirewallRuleDialog>;
|
||||
|
||||
setup(() => {
|
||||
account = {
|
||||
key: { providerId: 'azure', accountId: 'account1' },
|
||||
displayInfo: {
|
||||
contextualDisplayName: 'Microsoft Account',
|
||||
accountType: 'microsoft',
|
||||
displayName: 'Account 1',
|
||||
userId: 'user@email.com'
|
||||
},
|
||||
properties: [],
|
||||
isStale: false
|
||||
};
|
||||
|
||||
mockOnAddAccountErrorEvent = new Emitter<string>();
|
||||
mockOnCreateFirewallRule = new Emitter<void>();
|
||||
|
||||
// Create a mock firewall rule view model
|
||||
let firewallRuleViewModel = new FirewallRuleViewModel();
|
||||
mockFirewallRuleViewModel = TypeMoq.Mock.ofInstance(firewallRuleViewModel);
|
||||
mockFirewallRuleViewModel.setup(x => x.updateDefaultValues(TypeMoq.It.isAny()))
|
||||
.returns((ipAddress) => undefined);
|
||||
mockFirewallRuleViewModel.object.selectedAccount = account;
|
||||
mockFirewallRuleViewModel.object.isIPAddressSelected = true;
|
||||
|
||||
// Create a mocked out instantiation service
|
||||
instantiationService = TypeMoq.Mock.ofType(InstantiationService, TypeMoq.MockBehavior.Strict);
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(FirewallRuleViewModel)))
|
||||
.returns(() => mockFirewallRuleViewModel.object);
|
||||
|
||||
// Create a mock account picker
|
||||
let firewallRuleDialog = new FirewallRuleDialog(undefined!, undefined!, undefined!, instantiationService.object, undefined!, undefined!, new MockContextKeyService(), undefined!, undefined!, undefined!, undefined!);
|
||||
mockFirewallRuleDialog = TypeMoq.Mock.ofInstance(firewallRuleDialog);
|
||||
|
||||
let mockEvent = new Emitter<any>();
|
||||
mockFirewallRuleDialog.setup(x => x.onCancel)
|
||||
.returns(() => mockEvent.event);
|
||||
mockFirewallRuleDialog.setup(x => x.onCreateFirewallRule)
|
||||
.returns(() => mockOnCreateFirewallRule.event);
|
||||
mockFirewallRuleDialog.setup(x => x.onAddAccountErrorEvent)
|
||||
.returns((msg) => mockOnAddAccountErrorEvent.event);
|
||||
mockFirewallRuleDialog.setup(x => x.render());
|
||||
mockFirewallRuleDialog.setup(x => x.open());
|
||||
mockFirewallRuleDialog.setup(x => x.close());
|
||||
|
||||
instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(FirewallRuleDialog)))
|
||||
.returns(() => mockFirewallRuleDialog.object);
|
||||
|
||||
connectionProfile = {
|
||||
connectionName: 'new name',
|
||||
serverName: 'new server',
|
||||
databaseName: 'database',
|
||||
userName: 'user',
|
||||
password: 'password',
|
||||
authenticationType: '',
|
||||
savePassword: true,
|
||||
groupFullName: 'g2/g2-2',
|
||||
groupId: 'group id',
|
||||
getOptionsKey: () => '',
|
||||
matches: () => false,
|
||||
providerName: mssqlProviderName,
|
||||
options: {},
|
||||
saveProfile: true,
|
||||
id: ''
|
||||
};
|
||||
});
|
||||
|
||||
test('Add Account Failure - Error Message Shown', () => {
|
||||
// ... Create a mock instance of the error message service
|
||||
let errorMessageServiceStub = new TestErrorMessageService();
|
||||
let mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
|
||||
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()));
|
||||
|
||||
// ... Create instance of the controller with an opened dialog
|
||||
let controller = new FirewallRuleDialogController(instantiationService.object, undefined!, undefined!, mockErrorMessageService.object);
|
||||
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
|
||||
|
||||
// If: The firewall rule dialog reports a failure
|
||||
|
||||
mockOnAddAccountErrorEvent.fire('Error message');
|
||||
|
||||
// Then: An error dialog should have been opened
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
test('create firewall rule success', async () => {
|
||||
let deferredPromise = new Deferred();
|
||||
|
||||
mockFirewallRuleDialog.setup(x => x.onServiceComplete())
|
||||
.callback(() => {
|
||||
deferredPromise.resolve(true);
|
||||
});
|
||||
|
||||
// ... Create a mock instance of the account management test service
|
||||
let mockAccountManagementService = getMockAccountManagementService(true);
|
||||
|
||||
// ... Create a mock instance of the resource provider
|
||||
let mockResourceProvider = getMockResourceProvider(true, { result: true, errorMessage: '' });
|
||||
|
||||
// ... Create instance of the controller with an opened dialog
|
||||
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, undefined!);
|
||||
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
|
||||
|
||||
// If: The firewall rule dialog's create firewall rule get fired
|
||||
mockOnCreateFirewallRule.fire();
|
||||
|
||||
// Then: it should get security token from account management service and call create firewall rule in resource provider
|
||||
await deferredPromise;
|
||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockFirewallRuleDialog.verify(x => x.close(), TypeMoq.Times.once());
|
||||
mockFirewallRuleDialog.verify(x => x.onServiceComplete(), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
test('create firewall rule fails during getSecurity', async () => {
|
||||
let deferredPromise = new Deferred<{}>();
|
||||
|
||||
// ... Create a mock instance of the error message service
|
||||
let mockErrorMessageService = getMockErrorMessageService(deferredPromise);
|
||||
|
||||
// ... Create a mock instance of the account management test service
|
||||
let mockAccountManagementService = getMockAccountManagementService(false);
|
||||
|
||||
// ... Create a mock instance of the resource provider
|
||||
let mockResourceProvider = getMockResourceProvider(true, { result: true, errorMessage: '' });
|
||||
|
||||
// ... Create instance of the controller with an opened dialog
|
||||
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, mockErrorMessageService.object);
|
||||
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
|
||||
|
||||
// If: The firewall rule dialog's create firewall rule get fired
|
||||
mockOnCreateFirewallRule.fire();
|
||||
|
||||
// Then: it should get security token from account management service and an error dialog should have been opened
|
||||
await deferredPromise;
|
||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||
});
|
||||
|
||||
test('create firewall rule fails during createFirewallRule in ResourceProvider - result is false', async () => {
|
||||
let deferredPromise = new Deferred<{}>();
|
||||
|
||||
// ... Create a mock instance of the error message service
|
||||
let mockErrorMessageService = getMockErrorMessageService(deferredPromise);
|
||||
|
||||
// ... Create a mock instance of the account management test service
|
||||
let mockAccountManagementService = getMockAccountManagementService(true);
|
||||
|
||||
// ... Create a mock instance of the resource provider
|
||||
let mockResourceProvider = getMockResourceProvider(true, { result: false, errorMessage: '' });
|
||||
|
||||
// ... Create instance of the controller with an opened dialog
|
||||
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, mockErrorMessageService.object);
|
||||
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
|
||||
|
||||
// If: The firewall rule dialog's create firewall rule get fired
|
||||
mockOnCreateFirewallRule.fire();
|
||||
|
||||
// Then: it should get security token from account management service and an error dialog should have been opened
|
||||
await deferredPromise;
|
||||
|
||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
test('create firewall rule fails during createFirewallRule in ResourceProvider - reject promise', async () => {
|
||||
let deferredPromise = new Deferred<{}>();
|
||||
|
||||
// ... Create a mock instance of the error message service
|
||||
let mockErrorMessageService = getMockErrorMessageService(deferredPromise);
|
||||
|
||||
// ... Create a mock instance of the account management test service
|
||||
let mockAccountManagementService = getMockAccountManagementService(true);
|
||||
|
||||
// ... Create a mock instance of the resource provider
|
||||
let mockResourceProvider = getMockResourceProvider(false);
|
||||
|
||||
// ... Create instance of the controller with an opened dialog
|
||||
let controller = new FirewallRuleDialogController(instantiationService.object, mockResourceProvider.object, mockAccountManagementService.object, mockErrorMessageService.object);
|
||||
controller.openFirewallRuleDialog(connectionProfile, IPAddress, 'resourceID');
|
||||
|
||||
// If: The firewall rule dialog's create firewall rule get fired
|
||||
mockOnCreateFirewallRule.fire();
|
||||
|
||||
// Then: it should get security token from account management service and an error dialog should have been opened
|
||||
await deferredPromise;
|
||||
mockAccountManagementService.verify(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockResourceProvider.verify(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
mockErrorMessageService.verify(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
});
|
||||
|
||||
function getMockAccountManagementService(resolveSecurityToken: boolean): TypeMoq.Mock<TestAccountManagementService> {
|
||||
let accountManagementTestService = new TestAccountManagementService();
|
||||
let mockAccountManagementService = TypeMoq.Mock.ofInstance(accountManagementTestService);
|
||||
mockAccountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => resolveSecurityToken ? Promise.resolve({}) : Promise.reject(null));
|
||||
return mockAccountManagementService;
|
||||
}
|
||||
|
||||
function getMockResourceProvider(resolveCreateFirewallRule: boolean, response?: azdata.CreateFirewallRuleResponse): TypeMoq.Mock<TestResourceProvider> {
|
||||
let resourceProviderStub = new TestResourceProvider();
|
||||
let mockResourceProvider = TypeMoq.Mock.ofInstance(resourceProviderStub);
|
||||
mockResourceProvider.setup(x => x.createFirewallRule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => resolveCreateFirewallRule ? Promise.resolve(response) : Promise.reject(null));
|
||||
return mockResourceProvider;
|
||||
}
|
||||
|
||||
function getMockErrorMessageService(deferredPromise: Deferred<{}>): TypeMoq.Mock<TestErrorMessageService> {
|
||||
let errorMessageServiceStub = new TestErrorMessageService();
|
||||
let mockErrorMessageService = TypeMoq.Mock.ofInstance(errorMessageServiceStub);
|
||||
mockErrorMessageService.setup(x => x.showDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback(() => {
|
||||
deferredPromise.resolve(true);
|
||||
});
|
||||
return mockErrorMessageService;
|
||||
}
|
||||
176
src/sql/workbench/contrib/backup/browser/backup.component.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<form class="angular-form" #myForm="ngForm" (ngSubmit)="onSubmit(f)">
|
||||
<div class="angular-modal-body" style="display: flex; flex-direction: column;">
|
||||
<div class="angular-modal-body-content">
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.BACKUP_NAME}}
|
||||
</div>
|
||||
<div class="input-divider" #backupsetName>
|
||||
</div>
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.RECOVERY_MODEL}}
|
||||
</div>
|
||||
<div class="input-divider" #recoveryModelContainer>
|
||||
</div>
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.BACKUP_TYPE}}
|
||||
</div>
|
||||
<div class="input-divider" #backupTypeContainer>
|
||||
</div>
|
||||
<div class="input-divider check" #copyOnlyContainer>
|
||||
</div>
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.BACKUP_DEVICE}}
|
||||
</div>
|
||||
<div class="backup-path-list">
|
||||
<div #pathContainer>
|
||||
</div>
|
||||
</div>
|
||||
<table class="backup-path-table">
|
||||
<tr>
|
||||
<td style="padding-left: 0px; padding-right: 0px;">
|
||||
<div class="backup-path-button" #addPathContainer></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="backup-path-button" #removePathContainer></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="advanced-main-header" #advancedOptionContainer>
|
||||
<div class="advanced-main-body" #advancedOptionBodyContainer>
|
||||
<!-- Compression -->
|
||||
<div class="dialog-label advanced-header">
|
||||
{{localizedStrings.COMPRESSION}}
|
||||
</div>
|
||||
<div class="indent">
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.SET_BACKUP_COMPRESSION}}
|
||||
</div>
|
||||
<div class="dialog-label" #compressionContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Encryption -->
|
||||
<div class="dialog-label advanced-header">
|
||||
{{localizedStrings.ENCRYPTION}}
|
||||
</div>
|
||||
<div class="indent">
|
||||
<div class="option check" #encryptCheckContainer>
|
||||
</div>
|
||||
<div class="option" #encryptWarningContainer>
|
||||
<div class="sql icon warning">
|
||||
</div>
|
||||
<div class="warning-message">
|
||||
{{localizedStrings.NO_ENCRYPTOR_WARNING}}
|
||||
</div>
|
||||
</div>
|
||||
<div #encryptContainer>
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.ALGORITHM}}
|
||||
</div>
|
||||
<div class="dialog-label" #algorithmContainer>
|
||||
</div>
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.CERTIFICATE_OR_ASYMMETRIC_KEY}}
|
||||
</div>
|
||||
<div class="dialog-label" #encryptorContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overwrite media -->
|
||||
<div id="media" class="dialog-label advanced-header">
|
||||
{{localizedStrings.MEDIA}}
|
||||
</div>
|
||||
<div role="radiogroup" aria-labelledby="media" class="radio-indent">
|
||||
<div class="option">
|
||||
<input role="radio" type="radio" name="media-option" value="no_format" [checked]="!isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="isEncryptChecked" aria-labelledby="mediaOption"><span id="mediaOption">{{localizedStrings.MEDIA_OPTION}}</span>
|
||||
</div>
|
||||
<div role="radiogroup" aria-labelledby="mediaOption" style="margin-left:15px">
|
||||
<div class="option">
|
||||
<input role="radio" type="radio" name="existing-media" value="append" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked" aria-labelledby="existingMediaAppend"><span id="existingMediaAppend">{{localizedStrings.EXISTING_MEDIA_APPEND}}</span>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input role="radio" type="radio" name="existing-media" value="overwrite" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked" aria-labelledby="existingMediaOverwrite"><span id="existingMediaOverwrite">{{localizedStrings.EXISTING_MEDIA_OVERWRITE}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<input role="radio" type="radio" name="media-option" value="format" [checked]="isFormatChecked" (change)="onChangeMediaFormat()" aria-labelledby="mediaOptionFormat"><span id="mediaOptionFormat">{{localizedStrings.MEDIA_OPTION_FORMAT}}</span>
|
||||
</div>
|
||||
<div style="margin-left: 22px">
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.NEW_MEDIA_SET_NAME}}
|
||||
</div>
|
||||
<div class="dialog-label" #mediaName>
|
||||
</div>
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.NEW_MEDIA_SET_DESCRIPTION}}
|
||||
</div>
|
||||
<div class="dialog-label" #mediaDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction log -->
|
||||
<div id="transactionLog" class="dialog-label advanced-header">
|
||||
{{localizedStrings.TRANSACTION_LOG}}
|
||||
</div>
|
||||
<div role="radiogroup" aria-labelledby="transactionLog" class="radio-indent">
|
||||
<div class="option">
|
||||
<input role="radio" type="radio" name="t-log" value="truncate" [checked]="isTruncateChecked" (change)="onChangeTlog()" [disabled]="disableTlog" aria-labelledby="truncateTransaction"><span id="truncateTransaction">{{localizedStrings.TRUNCATE_TRANSACTION_LOG}}</span>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input role="radio" type="radio" name="t-log" value="taillog" [checked]="isTaillogChecked" (change)="onChangeTlog()" [disabled]="disableTlog" aria-labelledby="backupTail"><span id="backupTail">{{localizedStrings.BACKUP_TAIL}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reliability -->
|
||||
<div class="dialog-label advanced-header">
|
||||
{{localizedStrings.RELIABILITY}}
|
||||
</div>
|
||||
<div class="indent">
|
||||
<div class="option check" #checksumContainer>
|
||||
</div>
|
||||
<div class="option check" #verifyContainer>
|
||||
</div>
|
||||
<div class="option check" #continueOnErrorContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup expiration -->
|
||||
<div class="dialog-label advanced-header">
|
||||
{{localizedStrings.EXPIRATION}}
|
||||
</div>
|
||||
<div class="indent">
|
||||
<div class="dialog-label">
|
||||
{{localizedStrings.SET_BACKUP_RETAIN_DAYS}}
|
||||
</div>
|
||||
<div class="dialog-label">
|
||||
<div #backupDaysContainer></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" #modalFooterContainer>
|
||||
<div class="codicon in-progress" #inProgressContainer></div>
|
||||
<div class="right-footer">
|
||||
<div class="footer-button" #scriptButtonContainer>
|
||||
</div>
|
||||
<div class="footer-button" #backupButtonContainer>
|
||||
</div>
|
||||
<div class="footer-button" #cancelButtonContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
914
src/sql/workbench/contrib/backup/browser/backup.component.ts
Normal file
@@ -0,0 +1,914 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/backupDialog';
|
||||
|
||||
import { ElementRef, Component, Inject, forwardRef, ViewChild, ChangeDetectorRef } from '@angular/core';
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { ListBox } from 'sql/base/browser/ui/listBox/listBox';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { attachButtonStyler, attachListBoxStyler, attachInputBoxStyler, attachSelectBoxStyler, attachCheckboxStyler } from 'sql/platform/theme/common/styler';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import * as BackupConstants from 'sql/workbench/contrib/backup/common/constants';
|
||||
import { IBackupService, TaskExecutionMode } from 'sql/platform/backup/common/backupService';
|
||||
import * as FileValidationConstants from 'sql/workbench/services/fileBrowser/common/fileValidationServiceConstants';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController';
|
||||
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
|
||||
import * as cr from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
|
||||
export const BACKUP_SELECTOR: string = 'backup-component';
|
||||
|
||||
export class RestoreItemSource {
|
||||
restoreItemLocation: string;
|
||||
restoreItemDeviceType: number;
|
||||
isLogicalDevice: boolean;
|
||||
|
||||
constructor(location: any) {
|
||||
this.restoreItemDeviceType = location.restoreItemDeviceType;
|
||||
this.restoreItemLocation = location.restoreItemLocation;
|
||||
this.isLogicalDevice = location.isLogicalDevice;
|
||||
}
|
||||
}
|
||||
|
||||
interface MssqlBackupInfo {
|
||||
ownerUri: string;
|
||||
databaseName: string;
|
||||
backupType: number;
|
||||
backupComponent: number;
|
||||
backupDeviceType: number;
|
||||
selectedFiles: string;
|
||||
backupsetName: string;
|
||||
selectedFileGroup: { [path: string]: string };
|
||||
|
||||
// List of {key: backup path, value: device type}
|
||||
backupPathDevices: { [path: string]: number };
|
||||
backupPathList: [string];
|
||||
isCopyOnly: boolean;
|
||||
formatMedia: boolean;
|
||||
initialize: boolean;
|
||||
skipTapeHeader: boolean;
|
||||
mediaName: string;
|
||||
mediaDescription: string;
|
||||
checksum: boolean;
|
||||
continueAfterError: boolean;
|
||||
logTruncation: boolean;
|
||||
tailLogBackup: boolean;
|
||||
retainDays: number;
|
||||
compressionOption: number;
|
||||
verifyBackupRequired: boolean;
|
||||
encryptionAlgorithm: number;
|
||||
encryptorType: number;
|
||||
encryptorName: string;
|
||||
}
|
||||
|
||||
const LocalizedStrings = {
|
||||
BACKUP_NAME: localize('backup.backupName', "Backup name"),
|
||||
RECOVERY_MODEL: localize('backup.recoveryModel', "Recovery model"),
|
||||
BACKUP_TYPE: localize('backup.backupType', "Backup type"),
|
||||
BACKUP_DEVICE: localize('backup.backupDevice', "Backup files"),
|
||||
ALGORITHM: localize('backup.algorithm', "Algorithm"),
|
||||
CERTIFICATE_OR_ASYMMETRIC_KEY: localize('backup.certificateOrAsymmetricKey', "Certificate or Asymmetric key"),
|
||||
MEDIA: localize('backup.media', "Media"),
|
||||
MEDIA_OPTION: localize('backup.mediaOption', "Backup to the existing media set"),
|
||||
MEDIA_OPTION_FORMAT: localize('backup.mediaOptionFormat', "Backup to a new media set"),
|
||||
EXISTING_MEDIA_APPEND: localize('backup.existingMediaAppend', "Append to the existing backup set"),
|
||||
EXISTING_MEDIA_OVERWRITE: localize('backup.existingMediaOverwrite', "Overwrite all existing backup sets"),
|
||||
NEW_MEDIA_SET_NAME: localize('backup.newMediaSetName', "New media set name"),
|
||||
NEW_MEDIA_SET_DESCRIPTION: localize('backup.newMediaSetDescription', "New media set description"),
|
||||
CHECKSUM_CONTAINER: localize('backup.checksumContainer', "Perform checksum before writing to media"),
|
||||
VERIFY_CONTAINER: localize('backup.verifyContainer', "Verify backup when finished"),
|
||||
CONTINUE_ON_ERROR_CONTAINER: localize('backup.continueOnErrorContainer', "Continue on error"),
|
||||
EXPIRATION: localize('backup.expiration', "Expiration"),
|
||||
SET_BACKUP_RETAIN_DAYS: localize('backup.setBackupRetainDays', "Set backup retain days"),
|
||||
COPY_ONLY: localize('backup.copyOnly', "Copy-only backup"),
|
||||
ADVANCED_CONFIGURATION: localize('backup.advancedConfiguration', "Advanced Configuration"),
|
||||
COMPRESSION: localize('backup.compression', "Compression"),
|
||||
SET_BACKUP_COMPRESSION: localize('backup.setBackupCompression', "Set backup compression"),
|
||||
ENCRYPTION: localize('backup.encryption', "Encryption"),
|
||||
TRANSACTION_LOG: localize('backup.transactionLog', "Transaction log"),
|
||||
TRUNCATE_TRANSACTION_LOG: localize('backup.truncateTransactionLog', "Truncate the transaction log"),
|
||||
BACKUP_TAIL: localize('backup.backupTail', "Backup the tail of the log"),
|
||||
RELIABILITY: localize('backup.reliability', "Reliability"),
|
||||
MEDIA_NAME_REQUIRED_ERROR: localize('backup.mediaNameRequired', "Media name is required"),
|
||||
NO_ENCRYPTOR_WARNING: localize('backup.noEncryptorWarning', "No certificate or asymmetric key is available")
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: BACKUP_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./backup.component.html'))
|
||||
})
|
||||
export class BackupComponent extends AngularDisposable {
|
||||
@ViewChild('pathContainer', { read: ElementRef }) pathElement;
|
||||
@ViewChild('backupTypeContainer', { read: ElementRef }) backupTypeElement;
|
||||
@ViewChild('backupsetName', { read: ElementRef }) backupNameElement;
|
||||
@ViewChild('compressionContainer', { read: ElementRef }) compressionElement;
|
||||
@ViewChild('tlogOption', { read: ElementRef }) tlogOptionElement;
|
||||
@ViewChild('algorithmContainer', { read: ElementRef }) encryptionAlgorithmElement;
|
||||
@ViewChild('encryptorContainer', { read: ElementRef }) encryptorElement;
|
||||
@ViewChild('mediaName', { read: ElementRef }) mediaNameElement;
|
||||
@ViewChild('mediaDescription', { read: ElementRef }) mediaDescriptionElement;
|
||||
@ViewChild('recoveryModelContainer', { read: ElementRef }) recoveryModelElement;
|
||||
@ViewChild('backupDaysContainer', { read: ElementRef }) backupDaysElement;
|
||||
@ViewChild('backupButtonContainer', { read: ElementRef }) backupButtonElement;
|
||||
@ViewChild('cancelButtonContainer', { read: ElementRef }) cancelButtonElement;
|
||||
@ViewChild('addPathContainer', { read: ElementRef }) addPathElement;
|
||||
@ViewChild('removePathContainer', { read: ElementRef }) removePathElement;
|
||||
@ViewChild('copyOnlyContainer', { read: ElementRef }) copyOnlyElement;
|
||||
@ViewChild('encryptCheckContainer', { read: ElementRef }) encryptElement;
|
||||
@ViewChild('encryptContainer', { read: ElementRef }) encryptContainerElement;
|
||||
@ViewChild('verifyContainer', { read: ElementRef }) verifyElement;
|
||||
@ViewChild('checksumContainer', { read: ElementRef }) checksumElement;
|
||||
@ViewChild('continueOnErrorContainer', { read: ElementRef }) continueOnErrorElement;
|
||||
@ViewChild('encryptWarningContainer', { read: ElementRef }) encryptWarningElement;
|
||||
@ViewChild('inProgressContainer', { read: ElementRef }) inProgressElement;
|
||||
@ViewChild('modalFooterContainer', { read: ElementRef }) modalFooterElement;
|
||||
@ViewChild('scriptButtonContainer', { read: ElementRef }) scriptButtonElement;
|
||||
@ViewChild('advancedOptionContainer', { read: ElementRef }) advancedOptionElement;
|
||||
@ViewChild('advancedOptionBodyContainer', { read: ElementRef }) advancedOptionBodyElement;
|
||||
|
||||
private localizedStrings = LocalizedStrings;
|
||||
|
||||
private _uri: string;
|
||||
|
||||
private connection: IConnectionProfile;
|
||||
private databaseName: string;
|
||||
private defaultNewBackupFolder: string;
|
||||
private recoveryModel: string;
|
||||
private backupEncryptors;
|
||||
private containsBackupToUrl: boolean;
|
||||
|
||||
// UI element disable flag
|
||||
public disableTlog: boolean;
|
||||
|
||||
public selectedBackupComponent: string;
|
||||
private selectedFilesText: string;
|
||||
private selectedInitOption: string;
|
||||
private isTruncateChecked: boolean;
|
||||
private isTaillogChecked: boolean;
|
||||
private isFormatChecked: boolean;
|
||||
public isEncryptChecked: boolean;
|
||||
// Key: backup path, Value: device type
|
||||
private backupPathTypePairs: { [path: string]: number };
|
||||
|
||||
private compressionOptions = [BackupConstants.defaultCompression, BackupConstants.compressionOn, BackupConstants.compressionOff];
|
||||
private encryptionAlgorithms = [BackupConstants.aes128, BackupConstants.aes192, BackupConstants.aes256, BackupConstants.tripleDES];
|
||||
private existingMediaOptions = ['append', 'overwrite'];
|
||||
private backupTypeOptions: string[];
|
||||
|
||||
private backupTypeSelectBox: SelectBox;
|
||||
private backupNameBox: InputBox;
|
||||
private recoveryBox: InputBox;
|
||||
private backupRetainDaysBox: InputBox;
|
||||
private backupButton: Button;
|
||||
private cancelButton: Button;
|
||||
private scriptButton: Button;
|
||||
private addPathButton: Button;
|
||||
private removePathButton: Button;
|
||||
private pathListBox: ListBox;
|
||||
private compressionSelectBox: SelectBox;
|
||||
private algorithmSelectBox: SelectBox;
|
||||
private encryptorSelectBox: SelectBox;
|
||||
private mediaNameBox: InputBox;
|
||||
private mediaDescriptionBox: InputBox;
|
||||
private copyOnlyCheckBox: Checkbox;
|
||||
private encryptCheckBox: Checkbox;
|
||||
private verifyCheckBox: Checkbox;
|
||||
private checksumCheckBox: Checkbox;
|
||||
private continueOnErrorCheckBox: Checkbox;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
||||
@Inject(IFileBrowserDialogController) private fileBrowserDialogService: IFileBrowserDialogController,
|
||||
@Inject(IBackupUiService) private _backupUiService: IBackupUiService,
|
||||
@Inject(IBackupService) private _backupService: IBackupService,
|
||||
@Inject(IClipboardService) private clipboardService: IClipboardService,
|
||||
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
|
||||
) {
|
||||
super();
|
||||
this._backupUiService.onShowBackupEvent((param) => this.onGetBackupConfigInfo(param));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let self = this;
|
||||
this.addFooterButtons();
|
||||
|
||||
this.recoveryBox = new InputBox(this.recoveryModelElement.nativeElement, this.contextViewService, {
|
||||
placeholder: this.recoveryModel,
|
||||
ariaLabel: LocalizedStrings.RECOVERY_MODEL
|
||||
});
|
||||
// Set backup type
|
||||
this.backupTypeSelectBox = new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.BACKUP_TYPE });
|
||||
this.backupTypeSelectBox.render(this.backupTypeElement.nativeElement);
|
||||
|
||||
// Set copy-only check box
|
||||
this.copyOnlyCheckBox = new Checkbox(this.copyOnlyElement.nativeElement, {
|
||||
label: LocalizedStrings.COPY_ONLY,
|
||||
checked: false,
|
||||
onChange: (viaKeyboard) => { },
|
||||
ariaLabel: LocalizedStrings.COPY_ONLY
|
||||
});
|
||||
|
||||
// Encryption checkbox
|
||||
this.encryptCheckBox = new Checkbox(this.encryptElement.nativeElement, {
|
||||
label: LocalizedStrings.ENCRYPTION,
|
||||
checked: false,
|
||||
onChange: (viaKeyboard) => self.onChangeEncrypt(),
|
||||
ariaLabel: LocalizedStrings.ENCRYPTION
|
||||
});
|
||||
|
||||
// Verify backup checkbox
|
||||
this.verifyCheckBox = new Checkbox(this.verifyElement.nativeElement, {
|
||||
label: LocalizedStrings.VERIFY_CONTAINER,
|
||||
checked: false,
|
||||
onChange: (viaKeyboard) => { },
|
||||
ariaLabel: LocalizedStrings.VERIFY_CONTAINER
|
||||
});
|
||||
|
||||
// Perform checksum checkbox
|
||||
this.checksumCheckBox = new Checkbox(this.checksumElement.nativeElement, {
|
||||
label: LocalizedStrings.CHECKSUM_CONTAINER,
|
||||
checked: false,
|
||||
onChange: (viaKeyboard) => { },
|
||||
ariaLabel: LocalizedStrings.CHECKSUM_CONTAINER
|
||||
});
|
||||
|
||||
// Continue on error checkbox
|
||||
this.continueOnErrorCheckBox = new Checkbox(this.continueOnErrorElement.nativeElement, {
|
||||
label: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER,
|
||||
checked: false,
|
||||
onChange: (viaKeyboard) => { },
|
||||
ariaLabel: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER
|
||||
});
|
||||
|
||||
// Set backup name
|
||||
this.backupNameBox = new InputBox(this.backupNameElement.nativeElement, this.contextViewService, {
|
||||
ariaLabel: LocalizedStrings.BACKUP_NAME
|
||||
});
|
||||
|
||||
// Set backup path list
|
||||
this.pathListBox = new ListBox([], this.contextViewService);
|
||||
this.pathListBox.setAriaLabel(LocalizedStrings.BACKUP_DEVICE);
|
||||
this.pathListBox.onKeyDown(e => {
|
||||
if (this.pathListBox.selectedOptions.length > 0) {
|
||||
const key = e.keyCode;
|
||||
const ctrlOrCmd = e.ctrlKey || e.metaKey;
|
||||
|
||||
if (ctrlOrCmd && key === KeyCode.KEY_C) {
|
||||
let textToCopy = this.pathListBox.selectedOptions[0];
|
||||
for (let i = 1; i < this.pathListBox.selectedOptions.length; i++) {
|
||||
textToCopy = textToCopy + ', ' + this.pathListBox.selectedOptions[i];
|
||||
}
|
||||
|
||||
// Copy to clipboard
|
||||
this.clipboardService.writeText(textToCopy);
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.pathListBox.render(this.pathElement.nativeElement);
|
||||
|
||||
// Set backup path add/remove buttons
|
||||
this.addPathButton = this._register(new Button(this.addPathElement.nativeElement));
|
||||
this.addPathButton.label = '+';
|
||||
this.addPathButton.title = localize('addFile', "Add a file");
|
||||
|
||||
this.removePathButton = this._register(new Button(this.removePathElement.nativeElement));
|
||||
this.removePathButton.label = '-';
|
||||
this.removePathButton.title = localize('removeFile', "Remove files");
|
||||
|
||||
// Set compression
|
||||
this.compressionSelectBox = this._register(new SelectBox(this.compressionOptions, this.compressionOptions[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.SET_BACKUP_COMPRESSION }));
|
||||
this.compressionSelectBox.render(this.compressionElement.nativeElement);
|
||||
|
||||
// Set encryption
|
||||
this.algorithmSelectBox = this._register(new SelectBox(this.encryptionAlgorithms, this.encryptionAlgorithms[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.ALGORITHM }));
|
||||
this.algorithmSelectBox.render(this.encryptionAlgorithmElement.nativeElement);
|
||||
this.encryptorSelectBox = this._register(new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.CERTIFICATE_OR_ASYMMETRIC_KEY }));
|
||||
this.encryptorSelectBox.render(this.encryptorElement.nativeElement);
|
||||
|
||||
// Set media
|
||||
this.mediaNameBox = this._register(new InputBox(this.mediaNameElement.nativeElement,
|
||||
this.contextViewService,
|
||||
{
|
||||
validationOptions: {
|
||||
validation: (value: string) => !value ? ({ type: MessageType.ERROR, content: LocalizedStrings.MEDIA_NAME_REQUIRED_ERROR }) : null
|
||||
},
|
||||
ariaLabel: LocalizedStrings.NEW_MEDIA_SET_NAME
|
||||
}
|
||||
));
|
||||
|
||||
this.mediaDescriptionBox = this._register(new InputBox(this.mediaDescriptionElement.nativeElement, this.contextViewService, {
|
||||
ariaLabel: LocalizedStrings.NEW_MEDIA_SET_DESCRIPTION
|
||||
}));
|
||||
|
||||
// Set backup retain days
|
||||
let invalidInputMessage = localize('backupComponent.invalidInput', "Invalid input. Value must be greater than or equal 0.");
|
||||
this.backupRetainDaysBox = this._register(new InputBox(this.backupDaysElement.nativeElement,
|
||||
this.contextViewService,
|
||||
{
|
||||
placeholder: '0',
|
||||
type: 'number',
|
||||
min: '0',
|
||||
validationOptions: {
|
||||
validation: (value: string) => {
|
||||
if (types.isNumber(Number(value)) && Number(value) < 0) {
|
||||
return { type: MessageType.ERROR, content: invalidInputMessage };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
ariaLabel: LocalizedStrings.SET_BACKUP_RETAIN_DAYS
|
||||
}));
|
||||
|
||||
// Disable elements
|
||||
this.recoveryBox.disable();
|
||||
this.mediaNameBox.disable();
|
||||
this.mediaDescriptionBox.disable();
|
||||
|
||||
this.registerListeners();
|
||||
this.updateTheme(this.themeService.getTheme());
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this._backupUiService.onShowBackupDialog();
|
||||
}
|
||||
|
||||
private onGetBackupConfigInfo(param: { connection: IConnectionProfile, ownerUri: string }) {
|
||||
// Show spinner
|
||||
this.showSpinner();
|
||||
this.backupEnabled = false;
|
||||
|
||||
// Reset backup values
|
||||
this.backupNameBox.value = '';
|
||||
this.pathListBox.setOptions([], 0);
|
||||
|
||||
this.connection = param.connection;
|
||||
this._uri = param.ownerUri;
|
||||
|
||||
// Get backup configuration info
|
||||
this._backupService.getBackupConfigInfo(this._uri).then(configInfo => {
|
||||
if (configInfo) {
|
||||
this.defaultNewBackupFolder = configInfo.defaultBackupFolder;
|
||||
this.recoveryModel = configInfo.recoveryModel;
|
||||
this.backupEncryptors = configInfo.backupEncryptors;
|
||||
this.initialize(true);
|
||||
} else {
|
||||
this.initialize(false);
|
||||
}
|
||||
// Hide spinner
|
||||
this.hideSpinner();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show spinner in the backup dialog
|
||||
*/
|
||||
private showSpinner(): void {
|
||||
this.inProgressElement.nativeElement.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide spinner in the backup dialog
|
||||
*/
|
||||
private hideSpinner(): void {
|
||||
this.inProgressElement.nativeElement.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
private addFooterButtons(): void {
|
||||
// Set script footer button
|
||||
this.scriptButton = this._register(new Button(this.scriptButtonElement.nativeElement));
|
||||
this.scriptButton.label = localize('backupComponent.script', "Script");
|
||||
this.addButtonClickHandler(this.scriptButton, () => this.onScript());
|
||||
this._register(attachButtonStyler(this.scriptButton, this.themeService));
|
||||
this.scriptButton.enabled = false;
|
||||
|
||||
// Set backup footer button
|
||||
this.backupButton = this._register(new Button(this.backupButtonElement.nativeElement));
|
||||
this.backupButton.label = localize('backupComponent.backup', "Backup");
|
||||
this.addButtonClickHandler(this.backupButton, () => this.onOk());
|
||||
this._register(attachButtonStyler(this.backupButton, this.themeService));
|
||||
this.backupEnabled = false;
|
||||
|
||||
// Set cancel footer button
|
||||
this.cancelButton = this._register(new Button(this.cancelButtonElement.nativeElement));
|
||||
this.cancelButton.label = localize('backupComponent.cancel', "Cancel");
|
||||
this.addButtonClickHandler(this.cancelButton, () => this.onCancel());
|
||||
this._register(attachButtonStyler(this.cancelButton, this.themeService));
|
||||
}
|
||||
|
||||
private initialize(isMetadataPopulated: boolean): void {
|
||||
|
||||
this.databaseName = this.connection.databaseName;
|
||||
this.selectedBackupComponent = BackupConstants.labelDatabase;
|
||||
this.backupPathTypePairs = {};
|
||||
this.isFormatChecked = false;
|
||||
this.isEncryptChecked = false;
|
||||
this.selectedInitOption = this.existingMediaOptions[0];
|
||||
this.backupTypeOptions = [];
|
||||
|
||||
if (isMetadataPopulated) {
|
||||
this.backupEnabled = true;
|
||||
|
||||
// Set recovery model
|
||||
this.setControlsForRecoveryModel();
|
||||
|
||||
// Set backup type
|
||||
this.backupTypeSelectBox.setOptions(this.backupTypeOptions, 0);
|
||||
|
||||
this.setDefaultBackupName();
|
||||
this.backupNameBox.focus();
|
||||
|
||||
// Set backup path list
|
||||
this.setDefaultBackupPaths();
|
||||
let pathlist: ISelectOptionItem[] = [];
|
||||
for (let i in this.backupPathTypePairs) {
|
||||
pathlist.push({ text: i });
|
||||
}
|
||||
this.pathListBox.setOptions(pathlist, 0);
|
||||
|
||||
// Set encryption
|
||||
let encryptorItems = this.populateEncryptorCombo();
|
||||
this.encryptorSelectBox.setOptions(encryptorItems, 0);
|
||||
|
||||
if (encryptorItems.length === 0) {
|
||||
// Disable encryption checkbox
|
||||
this.encryptCheckBox.disable();
|
||||
|
||||
// Show warning instead of algorithm select boxes
|
||||
(<HTMLElement>this.encryptWarningElement.nativeElement).style.display = 'inline';
|
||||
(<HTMLElement>this.encryptContainerElement.nativeElement).style.display = 'none';
|
||||
}
|
||||
else {
|
||||
// Show algorithm select boxes instead of warning
|
||||
(<HTMLElement>this.encryptWarningElement.nativeElement).style.display = 'none';
|
||||
(<HTMLElement>this.encryptContainerElement.nativeElement).style.display = 'inline';
|
||||
|
||||
// Disable the algorithm select boxes since encryption is not checked by default
|
||||
this.setEncryptOptionsEnabled(false);
|
||||
}
|
||||
|
||||
this.setTLogOptions();
|
||||
|
||||
// disable elements
|
||||
this.recoveryBox.disable();
|
||||
this.mediaNameBox.disable();
|
||||
this.mediaDescriptionBox.disable();
|
||||
this.recoveryBox.value = this.recoveryModel;
|
||||
|
||||
// show warning message if latest backup file path contains url
|
||||
if (this.containsBackupToUrl) {
|
||||
this.pathListBox.setValidation(false, { content: localize('backup.containsBackupToUrlError', "Only backup to file is supported"), type: MessageType.WARNING });
|
||||
this.pathListBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset dialog controls to their initial state.
|
||||
*/
|
||||
private resetDialog(): void {
|
||||
this.isFormatChecked = false;
|
||||
this.isEncryptChecked = false;
|
||||
|
||||
this.copyOnlyCheckBox.checked = false;
|
||||
this.copyOnlyCheckBox.enable();
|
||||
this.compressionSelectBox.setOptions(this.compressionOptions, 0);
|
||||
this.encryptCheckBox.checked = false;
|
||||
this.encryptCheckBox.enable();
|
||||
this.onChangeEncrypt();
|
||||
|
||||
this.mediaNameBox.value = '';
|
||||
this.mediaDescriptionBox.value = '';
|
||||
this.checksumCheckBox.checked = false;
|
||||
this.verifyCheckBox.checked = false;
|
||||
this.continueOnErrorCheckBox.checked = false;
|
||||
this.backupRetainDaysBox.value = '0';
|
||||
this.algorithmSelectBox.setOptions(this.encryptionAlgorithms, 0);
|
||||
this.selectedInitOption = this.existingMediaOptions[0];
|
||||
this.containsBackupToUrl = false;
|
||||
this.pathListBox.setValidation(true);
|
||||
|
||||
this.cancelButton.applyStyles();
|
||||
this.scriptButton.applyStyles();
|
||||
this.backupButton.applyStyles();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachInputBoxStyler(this.backupNameBox, this.themeService));
|
||||
this._register(attachInputBoxStyler(this.recoveryBox, this.themeService));
|
||||
this._register(attachSelectBoxStyler(this.backupTypeSelectBox, this.themeService));
|
||||
this._register(attachListBoxStyler(this.pathListBox, this.themeService));
|
||||
this._register(attachButtonStyler(this.addPathButton, this.themeService));
|
||||
this._register(attachButtonStyler(this.removePathButton, this.themeService));
|
||||
this._register(attachSelectBoxStyler(this.compressionSelectBox, this.themeService));
|
||||
this._register(attachSelectBoxStyler(this.algorithmSelectBox, this.themeService));
|
||||
this._register(attachSelectBoxStyler(this.encryptorSelectBox, this.themeService));
|
||||
this._register(attachInputBoxStyler(this.mediaNameBox, this.themeService));
|
||||
this._register(attachInputBoxStyler(this.mediaDescriptionBox, this.themeService));
|
||||
this._register(attachInputBoxStyler(this.backupRetainDaysBox, this.themeService));
|
||||
this._register(attachCheckboxStyler(this.copyOnlyCheckBox, this.themeService));
|
||||
this._register(attachCheckboxStyler(this.encryptCheckBox, this.themeService));
|
||||
this._register(attachCheckboxStyler(this.verifyCheckBox, this.themeService));
|
||||
this._register(attachCheckboxStyler(this.checksumCheckBox, this.themeService));
|
||||
this._register(attachCheckboxStyler(this.continueOnErrorCheckBox, this.themeService));
|
||||
|
||||
this._register(this.backupTypeSelectBox.onDidSelect(selected => this.onBackupTypeChanged()));
|
||||
this.addButtonClickHandler(this.addPathButton, () => this.onAddClick());
|
||||
this.addButtonClickHandler(this.removePathButton, () => this.onRemoveClick());
|
||||
this._register(this.mediaNameBox.onDidChange(mediaName => {
|
||||
this.mediaNameChanged(mediaName);
|
||||
}));
|
||||
this._register(this.backupRetainDaysBox.onDidChange(days => {
|
||||
this.backupRetainDaysChanged(days);
|
||||
}));
|
||||
|
||||
this._register(this.themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
||||
}
|
||||
|
||||
// Update theming that is specific to backup dialog
|
||||
private updateTheme(theme: ITheme): void {
|
||||
// set modal footer style
|
||||
let footerHtmlElement: HTMLElement = <HTMLElement>this.modalFooterElement.nativeElement;
|
||||
const backgroundColor = theme.getColor(SIDE_BAR_BACKGROUND);
|
||||
const border = theme.getColor(cr.contrastBorder) ? theme.getColor(cr.contrastBorder).toString() : null;
|
||||
const footerBorderTopWidth = border ? '1px' : null;
|
||||
const footerBorderTopStyle = border ? 'solid' : null;
|
||||
footerHtmlElement.style.backgroundColor = backgroundColor ? backgroundColor.toString() : null;
|
||||
footerHtmlElement.style.borderTopWidth = footerBorderTopWidth;
|
||||
footerHtmlElement.style.borderTopStyle = footerBorderTopStyle;
|
||||
footerHtmlElement.style.borderTopColor = border;
|
||||
}
|
||||
|
||||
private addButtonClickHandler(button: Button, handler: () => void) {
|
||||
if (button && handler) {
|
||||
button.onDidClick(() => {
|
||||
if (button.enabled) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* UI event handlers
|
||||
*/
|
||||
private onScript(): void {
|
||||
this._backupService.backup(this._uri, this.createBackupInfo(), TaskExecutionMode.script);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private onOk(): void {
|
||||
this._backupService.backup(this._uri, this.createBackupInfo(), TaskExecutionMode.executeAndScript);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private onCancel(): void {
|
||||
this.close();
|
||||
this.connectionManagementService.disconnect(this._uri);
|
||||
}
|
||||
|
||||
private close(): void {
|
||||
this._backupUiService.closeBackup();
|
||||
this.resetDialog();
|
||||
}
|
||||
|
||||
public onChangeTlog(): void {
|
||||
this.isTruncateChecked = !this.isTruncateChecked;
|
||||
this.isTaillogChecked = !this.isTaillogChecked;
|
||||
this.detectChange();
|
||||
}
|
||||
|
||||
private onChangeEncrypt(): void {
|
||||
if (this.encryptCheckBox.checked) {
|
||||
this.setEncryptOptionsEnabled(true);
|
||||
|
||||
// Force to choose format media option since otherwise encryption cannot be done
|
||||
if (!this.isFormatChecked) {
|
||||
this.onChangeMediaFormat();
|
||||
}
|
||||
} else {
|
||||
this.setEncryptOptionsEnabled(false);
|
||||
}
|
||||
this.isEncryptChecked = this.encryptCheckBox.checked;
|
||||
this.detectChange();
|
||||
}
|
||||
|
||||
private onChangeMediaFormat(): void {
|
||||
this.isFormatChecked = !this.isFormatChecked;
|
||||
this.enableMediaInput(this.isFormatChecked);
|
||||
if (this.isFormatChecked) {
|
||||
if (strings.isFalsyOrWhitespace(this.mediaNameBox.value)) {
|
||||
this.backupEnabled = false;
|
||||
this.backupButton.enabled = false;
|
||||
this.mediaNameBox.showMessage({ type: MessageType.ERROR, content: LocalizedStrings.MEDIA_NAME_REQUIRED_ERROR });
|
||||
}
|
||||
} else {
|
||||
this.enableBackupButton();
|
||||
}
|
||||
this.detectChange();
|
||||
}
|
||||
|
||||
private set backupEnabled(value: boolean) {
|
||||
this.backupButton.enabled = value;
|
||||
this.scriptButton.enabled = value;
|
||||
}
|
||||
|
||||
private onBackupTypeChanged(): void {
|
||||
if (this.getSelectedBackupType() === BackupConstants.labelDifferential) {
|
||||
this.copyOnlyCheckBox.checked = false;
|
||||
this.copyOnlyCheckBox.disable();
|
||||
} else {
|
||||
this.copyOnlyCheckBox.enable();
|
||||
}
|
||||
|
||||
this.setTLogOptions();
|
||||
this.setDefaultBackupName();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
private onAddClick(): void {
|
||||
this.fileBrowserDialogService.showDialog(this._uri,
|
||||
this.defaultNewBackupFolder,
|
||||
BackupConstants.fileFiltersSet,
|
||||
FileValidationConstants.backup,
|
||||
false,
|
||||
(filepath => this.handlePathAdded(filepath)));
|
||||
}
|
||||
|
||||
private handlePathAdded(filepath: string) {
|
||||
if (filepath && !this.backupPathTypePairs[filepath]) {
|
||||
if ((this.getBackupPathCount() < BackupConstants.maxDevices)) {
|
||||
this.backupPathTypePairs[filepath] = BackupConstants.deviceTypeFile;
|
||||
this.pathListBox.add(filepath);
|
||||
this.enableBackupButton();
|
||||
this.enableAddRemoveButtons();
|
||||
|
||||
// stop showing error message if the list content was invalid due to no file path
|
||||
if (!this.pathListBox.isContentValid && this.pathListBox.count === 1) {
|
||||
this.pathListBox.setValidation(true);
|
||||
}
|
||||
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onRemoveClick(): void {
|
||||
let self = this;
|
||||
this.pathListBox.selectedOptions.forEach(selected => {
|
||||
if (self.backupPathTypePairs[selected]) {
|
||||
if (self.backupPathTypePairs[selected] === BackupConstants.deviceTypeURL) {
|
||||
// stop showing warning message since url path is getting removed
|
||||
this.pathListBox.setValidation(true);
|
||||
this.containsBackupToUrl = false;
|
||||
}
|
||||
|
||||
delete self.backupPathTypePairs[selected];
|
||||
}
|
||||
});
|
||||
|
||||
this.pathListBox.remove();
|
||||
if (this.pathListBox.count === 0) {
|
||||
this.backupEnabled = false;
|
||||
|
||||
// show input validation error
|
||||
this.pathListBox.setValidation(false, { content: localize('backup.backupFileRequired', "Backup file path is required"), type: MessageType.ERROR });
|
||||
this.pathListBox.focus();
|
||||
}
|
||||
|
||||
this.enableAddRemoveButtons();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
private enableAddRemoveButtons(): void {
|
||||
if (this.pathListBox.count === 0) {
|
||||
this.removePathButton.enabled = false;
|
||||
} else if (this.pathListBox.count === BackupConstants.maxDevices) {
|
||||
this.addPathButton.enabled = false;
|
||||
} else {
|
||||
this.removePathButton.enabled = true;
|
||||
this.addPathButton.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper methods
|
||||
*/
|
||||
private setControlsForRecoveryModel(): void {
|
||||
if (this.recoveryModel === BackupConstants.recoveryModelSimple) {
|
||||
this.selectedBackupComponent = BackupConstants.labelDatabase;
|
||||
}
|
||||
|
||||
this.populateBackupTypes();
|
||||
}
|
||||
|
||||
private populateBackupTypes(): void {
|
||||
this.backupTypeOptions.push(BackupConstants.labelFull);
|
||||
if (this.databaseName !== 'master') {
|
||||
this.backupTypeOptions.push(BackupConstants.labelDifferential);
|
||||
if (this.recoveryModel !== BackupConstants.recoveryModelSimple) {
|
||||
this.backupTypeOptions.push(BackupConstants.labelLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private populateEncryptorCombo(): string[] {
|
||||
let encryptorCombo = [];
|
||||
this.backupEncryptors.forEach((encryptor) => {
|
||||
let encryptorTypeStr = (encryptor.encryptorType === 0 ? BackupConstants.serverCertificate : BackupConstants.asymmetricKey);
|
||||
encryptorCombo.push(encryptor.encryptorName + '(' + encryptorTypeStr + ')');
|
||||
});
|
||||
return encryptorCombo;
|
||||
}
|
||||
|
||||
private setDefaultBackupName(): void {
|
||||
if (this.backupNameBox && (!this.backupNameBox.value || this.backupNameBox.value.trim().length === 0)) {
|
||||
let utc = new Date().toJSON().slice(0, 19);
|
||||
this.backupNameBox.value = this.databaseName + '-' + this.getSelectedBackupType() + '-' + utc;
|
||||
}
|
||||
}
|
||||
|
||||
private setDefaultBackupPaths(): void {
|
||||
if (this.defaultNewBackupFolder && this.defaultNewBackupFolder.length > 0) {
|
||||
|
||||
// TEMPORARY WORKAROUND: karlb 5/27 - try to guess path separator on server based on first character in path
|
||||
let serverPathSeparator: string = '\\';
|
||||
if (this.defaultNewBackupFolder[0] === '/') {
|
||||
serverPathSeparator = '/';
|
||||
}
|
||||
let d: Date = new Date();
|
||||
let formattedDateTime: string = `-${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}-${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`;
|
||||
let defaultNewBackupLocation = this.defaultNewBackupFolder + serverPathSeparator + this.databaseName + formattedDateTime + '.bak';
|
||||
|
||||
// Add a default new backup location
|
||||
this.backupPathTypePairs[defaultNewBackupLocation] = BackupConstants.deviceTypeFile;
|
||||
}
|
||||
}
|
||||
|
||||
private enableMediaInput(enable: boolean): void {
|
||||
if (enable) {
|
||||
this.mediaNameBox.enable();
|
||||
this.mediaDescriptionBox.enable();
|
||||
} else {
|
||||
this.mediaNameBox.disable();
|
||||
this.mediaDescriptionBox.disable();
|
||||
}
|
||||
}
|
||||
|
||||
private detectChange(): void {
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
private setTLogOptions(): void {
|
||||
if (this.getSelectedBackupType() === BackupConstants.labelLog) {
|
||||
// Enable log options
|
||||
this.disableTlog = false;
|
||||
// Choose the default option
|
||||
this.isTruncateChecked = true;
|
||||
} else {
|
||||
// Unselect log options
|
||||
this.isTruncateChecked = false;
|
||||
this.isTaillogChecked = false;
|
||||
// Disable log options
|
||||
this.disableTlog = true;
|
||||
}
|
||||
}
|
||||
|
||||
private getBackupTypeNumber(): number {
|
||||
let backupType;
|
||||
switch (this.getSelectedBackupType()) {
|
||||
case BackupConstants.labelFull:
|
||||
backupType = 0;
|
||||
break;
|
||||
case BackupConstants.labelDifferential:
|
||||
backupType = 1;
|
||||
break;
|
||||
case BackupConstants.labelLog:
|
||||
backupType = 2;
|
||||
break;
|
||||
}
|
||||
return backupType;
|
||||
}
|
||||
|
||||
private getBackupPathCount(): number {
|
||||
return this.pathListBox.count;
|
||||
}
|
||||
|
||||
private getSelectedBackupType(): string {
|
||||
let backupType = '';
|
||||
if (this.backupTypeSelectBox) {
|
||||
backupType = this.backupTypeSelectBox.value;
|
||||
}
|
||||
return backupType;
|
||||
}
|
||||
|
||||
private enableBackupButton(): void {
|
||||
if (!this.backupButton.enabled) {
|
||||
if (this.pathListBox.count > 0 && (!this.isFormatChecked || this.mediaNameBox.value) && this.backupRetainDaysBox.validate()) {
|
||||
this.backupEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setEncryptOptionsEnabled(enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.algorithmSelectBox.enable();
|
||||
this.encryptorSelectBox.enable();
|
||||
} else {
|
||||
this.algorithmSelectBox.disable();
|
||||
this.encryptorSelectBox.disable();
|
||||
}
|
||||
}
|
||||
|
||||
private mediaNameChanged(mediaName: string): void {
|
||||
if (!mediaName) {
|
||||
this.backupEnabled = false;
|
||||
} else {
|
||||
this.enableBackupButton();
|
||||
}
|
||||
}
|
||||
|
||||
private backupRetainDaysChanged(days: string): void {
|
||||
if (!this.backupRetainDaysBox.validate()) {
|
||||
this.backupEnabled = false;
|
||||
} else {
|
||||
this.enableBackupButton();
|
||||
}
|
||||
}
|
||||
|
||||
private createBackupInfo(): MssqlBackupInfo {
|
||||
let backupPathArray = [];
|
||||
for (let i in this.backupPathTypePairs) {
|
||||
backupPathArray.push(i);
|
||||
}
|
||||
|
||||
// get encryptor type and name
|
||||
let encryptorName = '';
|
||||
let encryptorType;
|
||||
|
||||
if (this.encryptCheckBox.checked && this.encryptorSelectBox.value !== '') {
|
||||
let selectedEncryptor = this.encryptorSelectBox.value;
|
||||
let encryptorTypeStr = selectedEncryptor.substring(selectedEncryptor.lastIndexOf('(') + 1, selectedEncryptor.lastIndexOf(')'));
|
||||
encryptorType = (encryptorTypeStr === BackupConstants.serverCertificate ? 0 : 1);
|
||||
encryptorName = selectedEncryptor.substring(0, selectedEncryptor.lastIndexOf('('));
|
||||
}
|
||||
|
||||
let backupInfo = <MssqlBackupInfo>{
|
||||
ownerUri: this._uri,
|
||||
databaseName: this.databaseName,
|
||||
backupType: this.getBackupTypeNumber(),
|
||||
backupComponent: 0,
|
||||
backupDeviceType: BackupConstants.backupDeviceTypeDisk,
|
||||
backupPathList: backupPathArray,
|
||||
selectedFiles: this.selectedFilesText,
|
||||
backupsetName: this.backupNameBox.value,
|
||||
selectedFileGroup: undefined,
|
||||
backupPathDevices: this.backupPathTypePairs,
|
||||
isCopyOnly: this.copyOnlyCheckBox.checked,
|
||||
|
||||
// Get advanced options
|
||||
formatMedia: this.isFormatChecked,
|
||||
initialize: (this.isFormatChecked ? true : (this.selectedInitOption === this.existingMediaOptions[1])),
|
||||
skipTapeHeader: this.isFormatChecked,
|
||||
mediaName: (this.isFormatChecked ? this.mediaNameBox.value : ''),
|
||||
mediaDescription: (this.isFormatChecked ? this.mediaDescriptionBox.value : ''),
|
||||
checksum: this.checksumCheckBox.checked,
|
||||
continueAfterError: this.continueOnErrorCheckBox.checked,
|
||||
logTruncation: this.isTruncateChecked,
|
||||
tailLogBackup: this.isTaillogChecked,
|
||||
retainDays: strings.isFalsyOrWhitespace(this.backupRetainDaysBox.value) ? 0 : this.backupRetainDaysBox.value,
|
||||
compressionOption: this.compressionOptions.indexOf(this.compressionSelectBox.value),
|
||||
verifyBackupRequired: this.verifyCheckBox.checked,
|
||||
encryptionAlgorithm: (this.encryptCheckBox.checked ? this.encryptionAlgorithms.indexOf(this.algorithmSelectBox.value) : 0),
|
||||
encryptorType: encryptorType,
|
||||
encryptorName: encryptorName
|
||||
};
|
||||
|
||||
return backupInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { TreeViewItemHandleArg } from 'sql/workbench/common/views';
|
||||
import { BackupAction } from 'sql/workbench/contrib/backup/browser/backupActions';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { ManageActionContext } from 'sql/workbench/browser/actions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext';
|
||||
import { MssqlNodeContext } from 'sql/workbench/contrib/dataExplorer/browser/mssqlNodeContext';
|
||||
import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { localize } from 'vs/nls';
|
||||
import { OEAction } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions';
|
||||
import { TreeNodeContextKey } from 'sql/workbench/contrib/objectExplorer/common/treeNodeContextKey';
|
||||
import { ConnectionContextKey } from 'sql/workbench/contrib/connection/common/connectionContextKey';
|
||||
import { ServerInfoContextKey } from 'sql/workbench/contrib/connection/common/serverInfoContextKey';
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
|
||||
new BackupAction().registerTask();
|
||||
|
||||
// data explorer
|
||||
const DE_BACKUP_COMMAND_ID = 'dataExplorer.backup';
|
||||
CommandsRegistry.registerCommand({
|
||||
id: DE_BACKUP_COMMAND_ID,
|
||||
handler: (accessor, args: TreeViewItemHandleArg) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(BackupAction.ID, args.$treeItem.payload);
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.DataExplorerContext, {
|
||||
group: 'connection',
|
||||
order: 4,
|
||||
command: {
|
||||
id: DE_BACKUP_COMMAND_ID,
|
||||
title: localize('backup', "Backup")
|
||||
},
|
||||
when: ContextKeyExpr.and(MssqlNodeContext.NodeProvider.isEqualTo(mssqlProviderName),
|
||||
MssqlNodeContext.NodeType.isEqualTo(NodeType.Database), MssqlNodeContext.IsCloud.toNegated(), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
||||
});
|
||||
|
||||
// oe
|
||||
const OE_BACKUP_COMMAND_ID = 'objectExplorer.backup';
|
||||
CommandsRegistry.registerCommand({
|
||||
id: OE_BACKUP_COMMAND_ID,
|
||||
handler: (accessor: ServicesAccessor, actionContext: any) => {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
return instantiationService.createInstance(OEAction, BackupAction.ID, BackupAction.LABEL).run(actionContext);
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, {
|
||||
group: 'connection',
|
||||
order: 4,
|
||||
command: {
|
||||
id: OE_BACKUP_COMMAND_ID,
|
||||
title: localize('backup', "Backup")
|
||||
},
|
||||
when: ContextKeyExpr.and(TreeNodeContextKey.NodeType.isEqualTo(NodeType.Database), ConnectionContextKey.Provider.isEqualTo(mssqlProviderName),
|
||||
ServerInfoContextKey.IsCloud.toNegated(), ServerInfoContextKey.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()))
|
||||
});
|
||||
|
||||
// dashboard explorer
|
||||
const ExplorerBackUpActionID = 'explorer.backup';
|
||||
CommandsRegistry.registerCommand(ExplorerBackUpActionID, (accessor, context: ManageActionContext) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
return commandService.executeCommand(BackupAction.ID, context.profile);
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.ExplorerWidgetContext, {
|
||||
command: {
|
||||
id: ExplorerBackUpActionID,
|
||||
title: BackupAction.LABEL
|
||||
},
|
||||
when: ContextKeyExpr.and(ItemContextKey.ItemType.isEqualTo('database'), ItemContextKey.ConnectionProvider.isEqualTo('mssql'),
|
||||
ItemContextKey.IsCloud.toNegated(), ItemContextKey.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString())),
|
||||
order: 2
|
||||
});
|
||||
55
src/sql/workbench/contrib/backup/browser/backup.module.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
ApplicationRef, ComponentFactoryResolver, NgModule,
|
||||
Inject, forwardRef, Type
|
||||
} from '@angular/core';
|
||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { providerIterator } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
|
||||
import { BackupComponent } from 'sql/workbench/contrib/backup/browser/backup.component';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
|
||||
|
||||
// Backup wizard main angular module
|
||||
export const BackupModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): Type<any> => {
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BackupComponent
|
||||
],
|
||||
entryComponents: [BackupComponent],
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
BrowserModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
{ provide: IBootstrapParams, useValue: params },
|
||||
{ provide: ISelector, useValue: selector },
|
||||
...providerIterator(instantiationService)
|
||||
]
|
||||
})
|
||||
class ModuleClass {
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(ISelector) private selector: string
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factory = this._resolver.resolveComponentFactory(BackupComponent);
|
||||
(<any>factory).factory.selector = this.selector;
|
||||
appRef.bootstrap(factory);
|
||||
}
|
||||
}
|
||||
|
||||
return ModuleClass;
|
||||
};
|
||||
71
src/sql/workbench/contrib/backup/browser/backupActions.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { getCurrentGlobalConnection } from 'sql/workbench/browser/taskUtilities';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
|
||||
import { Task } from 'sql/platform/tasks/browser/tasksRegistry';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
|
||||
export const BackupFeatureName = 'backup';
|
||||
|
||||
export function showBackup(accessor: ServicesAccessor, connection: IConnectionProfile): Promise<void> {
|
||||
const backupUiService = accessor.get(IBackupUiService);
|
||||
return backupUiService.showBackup(connection).then();
|
||||
}
|
||||
|
||||
export class BackupAction extends Task {
|
||||
public static readonly ID = BackupFeatureName;
|
||||
public static readonly LABEL = localize('backupAction.backup', "Backup");
|
||||
public static readonly ICON = BackupFeatureName;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: BackupAction.ID,
|
||||
title: BackupAction.LABEL,
|
||||
iconPath: undefined,
|
||||
iconClass: BackupAction.ICON
|
||||
});
|
||||
}
|
||||
|
||||
runTask(accessor: ServicesAccessor, profile: IConnectionProfile): void | Promise<void> {
|
||||
const configurationService = accessor.get<IConfigurationService>(IConfigurationService);
|
||||
const previewFeaturesEnabled: boolean = configurationService.getValue('workbench')['enablePreviewFeatures'];
|
||||
if (!previewFeaturesEnabled) {
|
||||
return accessor.get<INotificationService>(INotificationService).info(localize('backup.isPreviewFeature', "You must enable preview features in order to use backup"));
|
||||
}
|
||||
|
||||
const connectionManagementService = accessor.get<IConnectionManagementService>(IConnectionManagementService);
|
||||
if (!profile) {
|
||||
const objectExplorerService = accessor.get<IObjectExplorerService>(IObjectExplorerService);
|
||||
const workbenchEditorService = accessor.get<IEditorService>(IEditorService);
|
||||
profile = getCurrentGlobalConnection(objectExplorerService, connectionManagementService, workbenchEditorService);
|
||||
}
|
||||
if (profile) {
|
||||
const serverInfo = connectionManagementService.getServerInfo(profile.id);
|
||||
if (serverInfo && serverInfo.isCloud && profile.providerName === mssqlProviderName) {
|
||||
return accessor.get<INotificationService>(INotificationService).info(localize('backup.commandNotSupported', "Backup command is not supported for Azure SQL databases."));
|
||||
}
|
||||
|
||||
if (!profile.databaseName && profile.providerName === mssqlProviderName) {
|
||||
return accessor.get<INotificationService>(INotificationService).info(localize('backup.commandNotSupportedForServer', "Backup command is not supported in Server Context. Please select a Database and try again."));
|
||||
}
|
||||
}
|
||||
|
||||
const capabilitiesService = accessor.get(ICapabilitiesService);
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile);
|
||||
return instantiationService.invokeFunction(showBackup, profile);
|
||||
}
|
||||
}
|
||||
107
src/sql/workbench/contrib/backup/browser/backupDialog.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { BackupModule } from 'sql/workbench/contrib/backup/browser/backup.module';
|
||||
import { BACKUP_SELECTOR } from 'sql/workbench/contrib/backup/browser/backup.component';
|
||||
import { attachModalDialogStyler } from 'sql/platform/theme/common/styler';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { append, $ } from 'vs/base/browser/dom';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
|
||||
export class BackupDialog extends Modal {
|
||||
private _body: HTMLElement;
|
||||
private _backupTitle: string;
|
||||
private _moduleRef: any;
|
||||
|
||||
constructor(
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IClipboardService clipboardService: IClipboardService,
|
||||
@ILogService logService: ILogService,
|
||||
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
super('', TelemetryKeys.Backup, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { isAngular: true, hasErrors: true });
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
this._body = append(container, $('.backup-dialog'));
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
|
||||
// Add angular component template to dialog body
|
||||
this.bootstrapAngular(this._body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bootstrap params and perform the bootstrap
|
||||
*/
|
||||
private bootstrapAngular(bodyContainer: HTMLElement) {
|
||||
bootstrapAngular(this._instantiationService,
|
||||
BackupModule,
|
||||
bodyContainer,
|
||||
BACKUP_SELECTOR,
|
||||
undefined,
|
||||
undefined,
|
||||
(moduleRef) => this._moduleRef = moduleRef);
|
||||
}
|
||||
|
||||
public hideError() {
|
||||
this.showError('');
|
||||
}
|
||||
|
||||
public showError(err: string) {
|
||||
this.showError(err);
|
||||
}
|
||||
|
||||
/* Overwrite escape key behavior */
|
||||
protected onClose() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the module and DOM element and close the dialog
|
||||
*/
|
||||
public close() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
if (this._moduleRef) {
|
||||
this._moduleRef.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the dialog
|
||||
*/
|
||||
public open(connection: IConnectionProfile) {
|
||||
this._backupTitle = 'Backup database - ' + connection.serverName + ':' + connection.databaseName;
|
||||
this.title = this._backupTitle;
|
||||
this.show();
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
// Nothing currently laid out in this class
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.backup-path-list {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.backup-path-table {
|
||||
border: 0px;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
.backup-path-button {
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.backup-dialog {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.backup-dialog .advanced-main-header {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.backup-dialog .advanced-header {
|
||||
padding-top: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.backup-dialog input[type="checkbox"] {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.backup-dialog input[type="radio"] {
|
||||
margin-top: -2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.backup-dialog .indent {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.backup-dialog .radio-indent {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.backup-dialog .option {
|
||||
width: 100%;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.backup-dialog .option.check {
|
||||
display: flex;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.backup-dialog .check {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.backup-dialog .icon.warning {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.backup-dialog .warning-message{
|
||||
padding-left: 20px;
|
||||
}
|
||||
49
src/sql/workbench/contrib/backup/common/constants.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
// Constants
|
||||
export const maxDevices: number = 64;
|
||||
|
||||
// Constants for backup physical device type
|
||||
export const backupDeviceTypeDisk = 2;
|
||||
export const backupDeviceTypeTape = 5;
|
||||
export const backupDeviceTypeURL = 9;
|
||||
|
||||
// Constants for backup media device type
|
||||
export const deviceTypeLogicalDevice = 0;
|
||||
export const deviceTypeTape = 1;
|
||||
export const deviceTypeFile = 2;
|
||||
export const deviceTypeURL = 5;
|
||||
|
||||
export const recoveryModelSimple = 'Simple';
|
||||
export const recoveryModelFull = 'Full';
|
||||
|
||||
// Constants for UI strings
|
||||
export const labelDatabase = localize('backup.labelDatabase', "Database");
|
||||
export const labelFilegroup = localize('backup.labelFilegroup', "Files and filegroups");
|
||||
export const labelFull = localize('backup.labelFull', "Full");
|
||||
export const labelDifferential = localize('backup.labelDifferential', "Differential");
|
||||
export const labelLog = localize('backup.labelLog', "Transaction Log");
|
||||
export const labelDisk = localize('backup.labelDisk', "Disk");
|
||||
export const labelUrl = localize('backup.labelUrl', "Url");
|
||||
|
||||
export const defaultCompression = localize('backup.defaultCompression', "Use the default server setting");
|
||||
export const compressionOn = localize('backup.compressBackup', "Compress backup");
|
||||
export const compressionOff = localize('backup.doNotCompress', "Do not compress backup");
|
||||
|
||||
export const aes128 = 'AES 128';
|
||||
export const aes192 = 'AES 192';
|
||||
export const aes256 = 'AES 256';
|
||||
export const tripleDES = 'Triple DES';
|
||||
|
||||
export const serverCertificate = localize('backup.serverCertificate', "Server Certificate");
|
||||
export const asymmetricKey = localize('backup.asymmetricKey', "Asymmetric Key");
|
||||
|
||||
export const fileFiltersSet: { label: string, filters: string[] }[] = [
|
||||
{ label: localize('backup.filterBackupFiles', "Backup Files"), filters: ['*.bak', '*.trn', '*.log'] },
|
||||
{ label: localize('backup.allFiles', "All Files"), filters: ['*'] }
|
||||
];
|
||||
196
src/sql/workbench/contrib/charts/browser/actions.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsight } from 'sql/workbench/contrib/charts/browser/interfaces';
|
||||
import { Graph } from 'sql/workbench/contrib/charts/browser/graphInsight';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { QueryInput } from 'sql/workbench/contrib/query/common/queryInput';
|
||||
import { IInsightsConfig } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { IInsightOptions } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFileDialogService, FileFilter } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
|
||||
export interface IChartActionContext {
|
||||
options: IInsightOptions;
|
||||
insight: IInsight;
|
||||
}
|
||||
|
||||
export class CreateInsightAction extends Action {
|
||||
public static ID = 'chartview.createInsight';
|
||||
public static LABEL = localize('createInsightLabel', "Create Insight");
|
||||
public static ICON = 'createInsight';
|
||||
|
||||
constructor(
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService
|
||||
) {
|
||||
super(CreateInsightAction.ID, CreateInsightAction.LABEL, CreateInsightAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IChartActionContext): Promise<boolean> {
|
||||
let uriString: string = this.getActiveUriString();
|
||||
if (!uriString) {
|
||||
this.showError(localize('createInsightNoEditor', "Cannot create insight as the active editor is not a SQL Editor"));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
let uri: URI = URI.parse(uriString);
|
||||
let queryFile: string = uri.fsPath;
|
||||
let query: string = undefined;
|
||||
let type = {};
|
||||
let options = assign({}, context.options);
|
||||
delete options.type;
|
||||
type[context.options.type] = options;
|
||||
// create JSON
|
||||
let config: IInsightsConfig = {
|
||||
type,
|
||||
query,
|
||||
queryFile
|
||||
};
|
||||
|
||||
let widgetConfig = {
|
||||
name: localize('myWidgetName', "My-Widget"),
|
||||
gridItemConfig: {
|
||||
sizex: 2,
|
||||
sizey: 1
|
||||
},
|
||||
widget: {
|
||||
'insights-widget': config
|
||||
}
|
||||
};
|
||||
|
||||
let input = this.untitledEditorService.createOrGet(undefined, 'json', JSON.stringify(widgetConfig));
|
||||
|
||||
return this.editorService.openEditor(input, { pinned: true })
|
||||
.then(
|
||||
() => true,
|
||||
error => {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: error
|
||||
});
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getActiveUriString(): string {
|
||||
let editor = this.editorService.activeEditor;
|
||||
if (editor instanceof QueryInput) {
|
||||
return editor.uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private showError(errorMsg: string) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyAction extends Action {
|
||||
public static ID = 'chartview.copy';
|
||||
public static LABEL = localize('copyChartLabel', "Copy as image");
|
||||
public static ICON = 'copyImage';
|
||||
|
||||
constructor(
|
||||
@IClipboardService private clipboardService: IClipboardService,
|
||||
@INotificationService private notificationService: INotificationService
|
||||
) {
|
||||
super(CopyAction.ID, CopyAction.LABEL, CopyAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IChartActionContext): Promise<boolean> {
|
||||
if (context.insight instanceof Graph) {
|
||||
let data = context.insight.getCanvasData();
|
||||
if (!data) {
|
||||
this.showError(localize('chartNotFound', "Could not find chart to save"));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
this.clipboardService.writeImageDataUrl(data);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
private showError(errorMsg: string) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveImageAction extends Action {
|
||||
public static ID = 'chartview.saveImage';
|
||||
public static LABEL = localize('saveImageLabel', "Save as image");
|
||||
public static ICON = 'saveAsImage';
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IFileDialogService private readonly fileDialogService: IFileDialogService,
|
||||
@IOpenerService private readonly openerService: IOpenerService
|
||||
) {
|
||||
super(SaveImageAction.ID, SaveImageAction.LABEL, SaveImageAction.ICON);
|
||||
}
|
||||
|
||||
public async run(context: IChartActionContext): Promise<boolean> {
|
||||
if (context.insight instanceof Graph) {
|
||||
let fileFilters = new Array<FileFilter>({ extensions: ['png'], name: localize('resultsSerializer.saveAsFileExtensionPNGTitle', "PNG") });
|
||||
|
||||
const filePath = await this.fileDialogService.pickFileToSave({ filters: fileFilters });
|
||||
const data = (<Graph>context.insight).getCanvasData();
|
||||
if (!data) {
|
||||
this.notificationService.error(localize('chartNotFound', "Could not find chart to save"));
|
||||
return false;
|
||||
}
|
||||
if (filePath) {
|
||||
let buffer = this.decodeBase64Image(data);
|
||||
try {
|
||||
await this.fileService.writeFile(filePath, buffer);
|
||||
} catch (err) {
|
||||
if (err) {
|
||||
this.notificationService.error(err.message);
|
||||
} else {
|
||||
this.openerService.open(filePath, { openExternal: true });
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: localize('chartSaved', "Saved Chart to path: {0}", filePath.toString())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
private decodeBase64Image(data: string): VSBuffer {
|
||||
const marker = ';base64,';
|
||||
const raw = atob(data.substring(data.indexOf(marker) + marker.length));
|
||||
const n = raw.length;
|
||||
const a = new Uint8Array(new ArrayBuffer(n));
|
||||
for (let i = 0; i < n; i++) {
|
||||
a[i] = raw.charCodeAt(i);
|
||||
}
|
||||
return VSBuffer.wrap(a);
|
||||
}
|
||||
|
||||
}
|
||||
220
src/sql/workbench/contrib/charts/browser/chartOptions.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { IInsightOptions, DataDirection, DataType, LegendPosition, ChartType, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
export enum ControlType {
|
||||
combo,
|
||||
numberInput,
|
||||
input,
|
||||
checkbox,
|
||||
dateInput
|
||||
}
|
||||
|
||||
export interface IChartOption {
|
||||
label: string;
|
||||
type: ControlType;
|
||||
configEntry: string;
|
||||
default: any;
|
||||
options?: any[];
|
||||
displayableOptions?: string[];
|
||||
if?: (options: IInsightOptions) => boolean;
|
||||
}
|
||||
|
||||
export interface IChartOptions {
|
||||
general: Array<IChartOption>;
|
||||
[x: string]: Array<IChartOption>;
|
||||
}
|
||||
|
||||
const dataDirectionOption: IChartOption = {
|
||||
label: localize('dataDirectionLabel', "Data Direction"),
|
||||
type: ControlType.combo,
|
||||
displayableOptions: [localize('verticalLabel', "Vertical"), localize('horizontalLabel', "Horizontal")],
|
||||
options: [DataDirection.Vertical, DataDirection.Horizontal],
|
||||
configEntry: 'dataDirection',
|
||||
default: DataDirection.Horizontal
|
||||
};
|
||||
|
||||
const columnsAsLabelsInput: IChartOption = {
|
||||
label: localize('columnsAsLabelsLabel', "Use column names as labels"),
|
||||
type: ControlType.checkbox,
|
||||
configEntry: 'columnsAsLabels',
|
||||
default: true,
|
||||
if: (options: IInsightOptions) => {
|
||||
return options.dataDirection === DataDirection.Vertical && options.dataType !== DataType.Point;
|
||||
}
|
||||
};
|
||||
|
||||
const labelFirstColumnInput: IChartOption = {
|
||||
label: localize('labelFirstColumnLabel', "Use first column as row label"),
|
||||
type: ControlType.checkbox,
|
||||
configEntry: 'labelFirstColumn',
|
||||
default: false,
|
||||
if: (options: IInsightOptions) => {
|
||||
return options.dataDirection === DataDirection.Horizontal && options.dataType !== DataType.Point;
|
||||
}
|
||||
};
|
||||
|
||||
const legendInput: IChartOption = {
|
||||
label: localize('legendLabel', "Legend Position"),
|
||||
type: ControlType.combo,
|
||||
options: values(LegendPosition),
|
||||
configEntry: 'legendPosition',
|
||||
default: LegendPosition.Top
|
||||
};
|
||||
|
||||
const yAxisLabelInput: IChartOption = {
|
||||
label: localize('yAxisLabel', "Y Axis Label"),
|
||||
type: ControlType.input,
|
||||
configEntry: 'yAxisLabel',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const yAxisMinInput: IChartOption = {
|
||||
label: localize('yAxisMinVal', "Y Axis Minimum Value"),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'yAxisMin',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const yAxisMaxInput: IChartOption = {
|
||||
label: localize('yAxisMaxVal', "Y Axis Maximum Value"),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'yAxisMax',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisLabelInput: IChartOption = {
|
||||
label: localize('xAxisLabel', "X Axis Label"),
|
||||
type: ControlType.input,
|
||||
configEntry: 'xAxisLabel',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMinInput: IChartOption = {
|
||||
label: localize('xAxisMinVal', "X Axis Minimum Value"),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'xAxisMin',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMaxInput: IChartOption = {
|
||||
label: localize('xAxisMaxVal', "X Axis Maximum Value"),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'xAxisMax',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMinDateInput: IChartOption = {
|
||||
label: localize('xAxisMinDate', "X Axis Minimum Date"),
|
||||
type: ControlType.dateInput,
|
||||
configEntry: 'xAxisMin',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMaxDateInput: IChartOption = {
|
||||
label: localize('xAxisMaxDate', "X Axis Maximum Date"),
|
||||
type: ControlType.dateInput,
|
||||
configEntry: 'xAxisMax',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const dataTypeInput: IChartOption = {
|
||||
label: localize('dataTypeLabel', "Data Type"),
|
||||
type: ControlType.combo,
|
||||
options: [DataType.Number, DataType.Point],
|
||||
displayableOptions: [localize('numberLabel', "Number"), localize('pointLabel', "Point")],
|
||||
configEntry: 'dataType',
|
||||
default: DataType.Number
|
||||
};
|
||||
|
||||
export const ChartOptions: IChartOptions = {
|
||||
general: [
|
||||
{
|
||||
label: localize('chartTypeLabel', "Chart Type"),
|
||||
type: ControlType.combo,
|
||||
options: insightRegistry.getAllIds(),
|
||||
configEntry: 'type',
|
||||
default: ChartType.Bar
|
||||
}
|
||||
],
|
||||
[ChartType.Line]: [
|
||||
dataTypeInput,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
yAxisLabelInput,
|
||||
xAxisLabelInput,
|
||||
legendInput
|
||||
],
|
||||
[ChartType.Scatter]: [
|
||||
legendInput,
|
||||
yAxisLabelInput,
|
||||
xAxisLabelInput
|
||||
],
|
||||
[ChartType.TimeSeries]: [
|
||||
legendInput,
|
||||
yAxisLabelInput,
|
||||
yAxisMinInput,
|
||||
yAxisMaxInput,
|
||||
xAxisLabelInput,
|
||||
xAxisMinDateInput,
|
||||
xAxisMaxDateInput,
|
||||
],
|
||||
[ChartType.Bar]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput,
|
||||
yAxisLabelInput,
|
||||
yAxisMinInput,
|
||||
yAxisMaxInput,
|
||||
xAxisLabelInput
|
||||
],
|
||||
[ChartType.HorizontalBar]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput,
|
||||
xAxisLabelInput,
|
||||
xAxisMinInput,
|
||||
xAxisMaxInput,
|
||||
yAxisLabelInput
|
||||
],
|
||||
[ChartType.Pie]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput
|
||||
],
|
||||
[ChartType.Doughnut]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput
|
||||
],
|
||||
[InsightType.Table]: [],
|
||||
[InsightType.Count]: [],
|
||||
[InsightType.Image]: [
|
||||
{
|
||||
configEntry: 'encoding',
|
||||
label: localize('encodingOption', "Encoding"),
|
||||
type: ControlType.input,
|
||||
default: 'hex'
|
||||
},
|
||||
{
|
||||
configEntry: 'imageFormat',
|
||||
label: localize('imageFormatOption', "Image Format"),
|
||||
type: ControlType.input,
|
||||
default: 'jpeg'
|
||||
}
|
||||
]
|
||||
};
|
||||
37
src/sql/workbench/contrib/charts/browser/chartTab.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IPanelTab } from 'sql/base/browser/ui/panel/panel';
|
||||
import { ChartView } from './chartView';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class ChartTab implements IPanelTab {
|
||||
public readonly title = localize('chartTabTitle', "Chart");
|
||||
public readonly identifier = 'ChartTab';
|
||||
public readonly view: ChartView;
|
||||
|
||||
constructor(@IInstantiationService instantiationService: IInstantiationService) {
|
||||
this.view = instantiationService.createInstance(ChartView);
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this.view.queryRunner = runner;
|
||||
}
|
||||
|
||||
public chart(dataId: { batchId: number, resultId: number }): void {
|
||||
this.view.chart(dataId);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.view.dispose();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.view.clear();
|
||||
}
|
||||
}
|
||||
399
src/sql/workbench/contrib/charts/browser/chartView.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/chartView';
|
||||
|
||||
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
|
||||
import { Insight } from './insight';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { ChartOptions, IChartOption, ControlType } from './chartOptions';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/browser/insightRegistry';
|
||||
import { IInsightData } from './interfaces';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { attachSelectBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { CreateInsightAction, CopyAction, SaveImageAction, IChartActionContext } from 'sql/workbench/contrib/charts/browser/actions';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { ChartState, IInsightOptions, ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import * as nls from 'vs/nls';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
//Map used to store names and alternative names for chart types.
|
||||
//This is mainly used for comparison when options are parsed into the constructor.
|
||||
const altNameHash: { [oldName: string]: string } = {
|
||||
'horizontalBar': nls.localize('horizontalBarAltName', "Horizontal Bar"),
|
||||
'bar': nls.localize('barAltName', "Bar"),
|
||||
'line': nls.localize('lineAltName', "Line"),
|
||||
'pie': nls.localize('pieAltName', "Pie"),
|
||||
'scatter': nls.localize('scatterAltName', "Scatter"),
|
||||
'timeSeries': nls.localize('timeSeriesAltName', "Time Series"),
|
||||
'image': nls.localize('imageAltName', "Image"),
|
||||
'count': nls.localize('countAltName', "Count"),
|
||||
'table': nls.localize('tableAltName', "Table"),
|
||||
'doughnut': nls.localize('doughnutAltName', "Doughnut")
|
||||
};
|
||||
|
||||
export class ChartView extends Disposable implements IPanelView {
|
||||
private insight: Insight;
|
||||
private _queryRunner: QueryRunner;
|
||||
private _data: IInsightData;
|
||||
private _currentData: { batchId: number, resultId: number };
|
||||
private taskbar: Taskbar;
|
||||
|
||||
private _createInsightAction: CreateInsightAction;
|
||||
private _copyAction: CopyAction;
|
||||
private _saveAction: SaveImageAction;
|
||||
|
||||
private _state: ChartState;
|
||||
|
||||
private options: IInsightOptions = {
|
||||
type: ChartType.Bar
|
||||
};
|
||||
|
||||
|
||||
/** parent container */
|
||||
private container: HTMLElement;
|
||||
/** container for the options controls */
|
||||
private optionsControl: HTMLElement;
|
||||
/** container for type specific controls */
|
||||
private typeControls: HTMLElement;
|
||||
/** container for the insight */
|
||||
private insightContainer: HTMLElement;
|
||||
/** container for the action bar */
|
||||
private taskbarContainer: HTMLElement;
|
||||
/** container for the charting (includes insight and options) */
|
||||
private chartingContainer: HTMLElement;
|
||||
|
||||
private optionDisposables: IDisposable[] = [];
|
||||
private optionMap: { [x: string]: { element: HTMLElement; set: (val) => void } } = {};
|
||||
|
||||
constructor(
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IThemeService private _themeService: IThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this.taskbarContainer = DOM.$('div.taskbar-container');
|
||||
this.taskbar = new Taskbar(this.taskbarContainer);
|
||||
this.optionsControl = DOM.$('div.options-container');
|
||||
const generalControls = DOM.$('div.general-controls');
|
||||
this.optionsControl.appendChild(generalControls);
|
||||
this.typeControls = DOM.$('div.type-controls');
|
||||
this.optionsControl.appendChild(this.typeControls);
|
||||
|
||||
this._createInsightAction = this._instantiationService.createInstance(CreateInsightAction);
|
||||
this._copyAction = this._instantiationService.createInstance(CopyAction);
|
||||
this._saveAction = this._instantiationService.createInstance(SaveImageAction);
|
||||
|
||||
this.taskbar.setContent([{ action: this._createInsightAction }]);
|
||||
|
||||
const self = this;
|
||||
this.options = new Proxy(this.options, {
|
||||
get: function (target, key) {
|
||||
return target[key];
|
||||
},
|
||||
set: function (target, key, value) {
|
||||
let change = false;
|
||||
if (target[key] !== value) {
|
||||
change = true;
|
||||
}
|
||||
|
||||
target[key] = value;
|
||||
// mirror the change in our state
|
||||
if (self.state) {
|
||||
self.state.options[key] = value;
|
||||
}
|
||||
|
||||
if (change) {
|
||||
self.taskbar.context = <IChartActionContext>{ options: self.options, insight: self.insight ? self.insight.insight : undefined };
|
||||
if (key === 'type') {
|
||||
self.buildOptions();
|
||||
} else {
|
||||
self.verifyOptions();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}) as IInsightOptions;
|
||||
|
||||
|
||||
ChartOptions.general[0].options = insightRegistry.getAllIds();
|
||||
ChartOptions.general.map(o => {
|
||||
this.createOption(o, generalControls);
|
||||
});
|
||||
this.buildOptions();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to generate list of alternative names for use with SelectBox
|
||||
* @param option - the original option names.
|
||||
*/
|
||||
private changeToAltNames(option: string[]): string[] {
|
||||
return option.map(o => altNameHash[o] || o);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.optionDisposables);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
if (!this.container) {
|
||||
this.container = DOM.$('div.chart-parent-container');
|
||||
this.insightContainer = DOM.$('div.insight-container');
|
||||
this.chartingContainer = DOM.$('div.charting-container');
|
||||
this.container.appendChild(this.taskbarContainer);
|
||||
this.container.appendChild(this.chartingContainer);
|
||||
this.chartingContainer.appendChild(this.insightContainer);
|
||||
this.chartingContainer.appendChild(this.optionsControl);
|
||||
this.insight = new Insight(this.insightContainer, this.options, this._instantiationService);
|
||||
}
|
||||
|
||||
container.appendChild(this.container);
|
||||
|
||||
if (this._data) {
|
||||
this.insight.data = this._data;
|
||||
} else {
|
||||
this.queryRunner = this._queryRunner;
|
||||
}
|
||||
this.verifyOptions();
|
||||
}
|
||||
|
||||
public chart(dataId: { batchId: number, resultId: number }) {
|
||||
this.state.dataId = dataId;
|
||||
this._currentData = dataId;
|
||||
this.shouldGraph();
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
if (this.insight) {
|
||||
this.insight.layout(new DOM.Dimension(DOM.getContentWidth(this.insightContainer), DOM.getContentHeight(this.insightContainer)));
|
||||
}
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this._queryRunner = runner;
|
||||
this.shouldGraph();
|
||||
}
|
||||
|
||||
private shouldGraph() {
|
||||
// Check if we have the necessary information
|
||||
if (this._currentData && this._queryRunner) {
|
||||
// check if we are being asked to graph something that is available
|
||||
let batch = this._queryRunner.batchSets[this._currentData.batchId];
|
||||
if (batch) {
|
||||
let summary = batch.resultSetSummaries[this._currentData.resultId];
|
||||
if (summary) {
|
||||
this._queryRunner.getQueryRows(0, summary.rowCount, 0, 0).then(d => {
|
||||
this._data = {
|
||||
columns: summary.columnInfo.map(c => c.columnName),
|
||||
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
|
||||
};
|
||||
if (this.insight) {
|
||||
this.insight.data = this._data;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// if we have the necessary information but the information isn't available yet,
|
||||
// we should be smart and retrying when the information might be available
|
||||
}
|
||||
}
|
||||
|
||||
private buildOptions() {
|
||||
// The first element in the disposables list is for the chart type: the master dropdown that controls other option controls.
|
||||
// whiling rebuilding the options we should not dispose it, otherwise it would react to the theme change event
|
||||
if (this.optionDisposables.length > 1) { // this logic needs to be rewritten
|
||||
dispose(this.optionDisposables.slice(1));
|
||||
this.optionDisposables.splice(1);
|
||||
}
|
||||
|
||||
this.optionMap = {
|
||||
'type': this.optionMap['type']
|
||||
};
|
||||
DOM.clearNode(this.typeControls);
|
||||
|
||||
this.updateActionbar();
|
||||
ChartOptions[this.options.type].map(o => {
|
||||
this.createOption(o, this.typeControls);
|
||||
});
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
this.verifyOptions();
|
||||
}
|
||||
|
||||
private verifyOptions() {
|
||||
this.updateActionbar();
|
||||
for (let key in this.optionMap) {
|
||||
if (this.optionMap.hasOwnProperty(key)) {
|
||||
let option = find(ChartOptions[this.options.type], e => e.configEntry === key);
|
||||
if (option && option.if) {
|
||||
if (option.if(this.options)) {
|
||||
DOM.show(this.optionMap[key].element);
|
||||
} else {
|
||||
DOM.hide(this.optionMap[key].element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateActionbar() {
|
||||
if (this.insight && this.insight.isCopyable) {
|
||||
this.taskbar.context = { insight: this.insight.insight, options: this.options };
|
||||
this.taskbar.setContent([
|
||||
{ action: this._createInsightAction },
|
||||
{ action: this._copyAction },
|
||||
{ action: this._saveAction }
|
||||
]);
|
||||
} else {
|
||||
this.taskbar.setContent([{ action: this._createInsightAction }]);
|
||||
}
|
||||
}
|
||||
|
||||
private createOption(option: IChartOption, container: HTMLElement) {
|
||||
let label = DOM.$('div');
|
||||
label.innerText = option.label;
|
||||
let optionContainer = DOM.$('div.option-container');
|
||||
optionContainer.appendChild(label);
|
||||
let setFunc: (val) => void;
|
||||
let value = this.state ? this.state.options[option.configEntry] || option.default : option.default;
|
||||
switch (option.type) {
|
||||
case ControlType.checkbox:
|
||||
let checkbox = new Checkbox(optionContainer, {
|
||||
label: '',
|
||||
ariaLabel: option.label,
|
||||
checked: value,
|
||||
onChange: () => {
|
||||
if (this.options[option.configEntry] !== checkbox.checked) {
|
||||
this.options[option.configEntry] = checkbox.checked;
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: boolean) => {
|
||||
checkbox.checked = val;
|
||||
};
|
||||
break;
|
||||
case ControlType.combo:
|
||||
//pass options into changeAltNames in order for SelectBox to show user-friendly names.
|
||||
let dropdown = new SelectBox(option.displayableOptions || this.changeToAltNames(option.options), undefined, this._contextViewService);
|
||||
dropdown.select(option.options.indexOf(value));
|
||||
dropdown.render(optionContainer);
|
||||
dropdown.onDidSelect(e => {
|
||||
if (this.options[option.configEntry] !== option.options[e.index]) {
|
||||
this.options[option.configEntry] = option.options[e.index];
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
dropdown.select(option.options.indexOf(val));
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachSelectBoxStyler(dropdown, this._themeService));
|
||||
break;
|
||||
case ControlType.input:
|
||||
let input = new InputBox(optionContainer, this._contextViewService);
|
||||
input.value = value || '';
|
||||
input.onDidChange(e => {
|
||||
if (this.options[option.configEntry] !== e) {
|
||||
this.options[option.configEntry] = e;
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
input.value = val;
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachInputBoxStyler(input, this._themeService));
|
||||
break;
|
||||
case ControlType.numberInput:
|
||||
let numberInput = new InputBox(optionContainer, this._contextViewService, { type: 'number' });
|
||||
numberInput.value = value || '';
|
||||
numberInput.onDidChange(e => {
|
||||
if (this.options[option.configEntry] !== Number(e)) {
|
||||
this.options[option.configEntry] = Number(e);
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
numberInput.value = val;
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService));
|
||||
break;
|
||||
case ControlType.dateInput:
|
||||
let dateInput = new InputBox(optionContainer, this._contextViewService, { type: 'datetime-local' });
|
||||
dateInput.value = value || '';
|
||||
dateInput.onDidChange(e => {
|
||||
if (this.options[option.configEntry] !== e) {
|
||||
this.options[option.configEntry] = e;
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
dateInput.value = val;
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachInputBoxStyler(dateInput, this._themeService));
|
||||
break;
|
||||
}
|
||||
this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc };
|
||||
container.appendChild(optionContainer);
|
||||
this.options[option.configEntry] = value;
|
||||
}
|
||||
|
||||
public set state(val: ChartState) {
|
||||
this._state = val;
|
||||
if (this.state.options) {
|
||||
for (let key in this.state.options) {
|
||||
if (this.state.options.hasOwnProperty(key) && this.optionMap[key]) {
|
||||
this.options[key] = this.state.options[key];
|
||||
this.optionMap[key].set(this.state.options[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.state.dataId) {
|
||||
this.chart(this.state.dataId);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): ChartState {
|
||||
return this._state;
|
||||
}
|
||||
}
|
||||
45
src/sql/workbench/contrib/charts/browser/countInsight.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/countInsight';
|
||||
|
||||
import { IInsight, IInsightData } from './interfaces';
|
||||
|
||||
import { $, clearNode } from 'vs/base/browser/dom';
|
||||
import { InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export class CountInsight implements IInsight {
|
||||
public options;
|
||||
public static readonly types = [InsightType.Count];
|
||||
public readonly types = CountInsight.types;
|
||||
|
||||
private countImage: HTMLElement;
|
||||
|
||||
constructor(container: HTMLElement, options: any) {
|
||||
this.countImage = $('div');
|
||||
container.appendChild(this.countImage);
|
||||
}
|
||||
|
||||
public layout() { }
|
||||
|
||||
set data(data: IInsightData) {
|
||||
clearNode(this.countImage);
|
||||
|
||||
for (let i = 0; i < data.columns.length; i++) {
|
||||
let container = $('div.count-label-container');
|
||||
let label = $('span.label-container');
|
||||
label.innerText = data.columns[i];
|
||||
let value = $('span.value-container');
|
||||
value.innerText = data.rows[0][i];
|
||||
container.appendChild(label);
|
||||
container.appendChild(value);
|
||||
this.countImage.appendChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
}
|
||||
}
|
||||
441
src/sql/workbench/contrib/charts/browser/graphInsight.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as chartjs from 'chart.js';
|
||||
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
import { IInsight, IInsightData, IPointDataSet, customMixin } from './interfaces';
|
||||
import { IInsightOptions, DataDirection, ChartType, LegendPosition, DataType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
const noneLineGraphs = [ChartType.Doughnut, ChartType.Pie];
|
||||
|
||||
const timeSeriesScales: chartjs.ChartOptions = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
display: true,
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const defaultOptions: IInsightOptions = {
|
||||
type: ChartType.Bar,
|
||||
dataDirection: DataDirection.Horizontal
|
||||
};
|
||||
|
||||
export class Graph implements IInsight {
|
||||
private _options: IInsightOptions;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private chartjs: chartjs;
|
||||
private _data: IInsightData;
|
||||
|
||||
private originalType: ChartType;
|
||||
|
||||
public static readonly types = [ChartType.Bar, ChartType.Doughnut, ChartType.HorizontalBar, ChartType.Line, ChartType.Pie, ChartType.Scatter, ChartType.TimeSeries];
|
||||
public readonly types = Graph.types;
|
||||
|
||||
private _theme: ITheme;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement, options: IInsightOptions = defaultOptions,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
this._theme = themeService.getTheme();
|
||||
themeService.onThemeChange(e => {
|
||||
this._theme = e;
|
||||
this.data = this._data;
|
||||
});
|
||||
this.options = mixin(options, defaultOptions, false);
|
||||
|
||||
let canvasContainer = document.createElement('div');
|
||||
canvasContainer.style.width = '100%';
|
||||
canvasContainer.style.height = '100%';
|
||||
|
||||
this.canvas = document.createElement('canvas');
|
||||
canvasContainer.appendChild(this.canvas);
|
||||
|
||||
container.appendChild(canvasContainer);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
|
||||
public getCanvasData(): string {
|
||||
return this.chartjs.toBase64Image();
|
||||
}
|
||||
|
||||
public set data(data: IInsightData) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
let labels: Array<string>;
|
||||
let chartData: Array<Chart.ChartDataSets>;
|
||||
|
||||
if (this.options.dataDirection === DataDirection.Horizontal) {
|
||||
if (this.options.labelFirstColumn) {
|
||||
labels = data.columns.slice(1);
|
||||
} else {
|
||||
labels = data.columns;
|
||||
}
|
||||
} else {
|
||||
labels = data.rows.map(row => row[0]);
|
||||
}
|
||||
|
||||
if (this.originalType === ChartType.TimeSeries) {
|
||||
let dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
let legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) });
|
||||
}
|
||||
});
|
||||
chartData = values(dataSetMap);
|
||||
} else {
|
||||
if (this.options.dataDirection === DataDirection.Horizontal) {
|
||||
if (this.options.labelFirstColumn) {
|
||||
chartData = data.rows.map((row) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)).slice(1),
|
||||
label: row[0]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
chartData = data.rows.map((row, i) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)),
|
||||
label: localize('series', "Series {0}", i)
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.options.columnsAsLabels) {
|
||||
chartData = data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: data.rows.map(row => Number(row[i + 1])),
|
||||
label: data.columns[i + 1]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
chartData = data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: data.rows.map(row => Number(row[i + 1])),
|
||||
label: localize('series', "Series {0}", i + 1)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chartData = chartData.map((c, i) => {
|
||||
return mixin(c, getColors(this.options.type, i, c.data.length), false);
|
||||
});
|
||||
|
||||
if (this.chartjs) {
|
||||
this.chartjs.data.datasets = chartData;
|
||||
this.chartjs.config.type = this.options.type;
|
||||
// we don't want to include lables for timeSeries
|
||||
this.chartjs.data.labels = this.originalType === 'timeSeries' ? [] : labels;
|
||||
this.chartjs.config.options = this.transformOptions(this.options);
|
||||
this.chartjs.update(0);
|
||||
} else {
|
||||
this.chartjs = new chartjs.Chart(this.canvas.getContext('2d'), {
|
||||
data: {
|
||||
// we don't want to include lables for timeSeries
|
||||
labels: this.originalType === 'timeSeries' ? [] : labels,
|
||||
datasets: chartData
|
||||
},
|
||||
type: this.options.type,
|
||||
options: this.transformOptions(this.options)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private transformOptions(options: IInsightOptions): Chart.ChartOptions {
|
||||
let retval: Chart.ChartOptions = {};
|
||||
retval.maintainAspectRatio = false;
|
||||
|
||||
let foregroundColor = this._theme.getColor(colors.editorForeground);
|
||||
let foreground = foregroundColor ? foregroundColor.toString() : null;
|
||||
let gridLinesColor = this._theme.getColor(editorLineNumbers);
|
||||
let gridLines = gridLinesColor ? gridLinesColor.toString() : null;
|
||||
let backgroundColor = this._theme.getColor(colors.editorBackground);
|
||||
let background = backgroundColor ? backgroundColor.toString() : null;
|
||||
|
||||
if (options) {
|
||||
retval.scales = {};
|
||||
// we only want to include axis if it is a axis based graph type
|
||||
if (!find(noneLineGraphs, x => x === options.type as ChartType)) {
|
||||
retval.scales.xAxes = [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground,
|
||||
labelString: options.xAxisLabel,
|
||||
display: options.xAxisLabel ? true : false
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}];
|
||||
|
||||
if (options.xAxisMax) {
|
||||
retval.scales = mixin(retval.scales, { xAxes: [{ ticks: { max: options.xAxisMax } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
if (options.xAxisMin) {
|
||||
retval.scales = mixin(retval.scales, { xAxes: [{ ticks: { min: options.xAxisMin } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
retval.scales.yAxes = [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground,
|
||||
labelString: options.yAxisLabel,
|
||||
display: options.yAxisLabel ? true : false
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}];
|
||||
|
||||
if (options.yAxisMax) {
|
||||
retval.scales = mixin(retval.scales, { yAxes: [{ ticks: { max: options.yAxisMax } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
if (options.yAxisMin) {
|
||||
retval.scales = mixin(retval.scales, { yAxes: [{ ticks: { min: options.yAxisMin } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
if (this.originalType === ChartType.TimeSeries) {
|
||||
retval = mixin(retval, timeSeriesScales, true, customMixin);
|
||||
if (options.xAxisMax) {
|
||||
retval = mixin(retval, {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
time: {
|
||||
max: options.xAxisMax
|
||||
}
|
||||
}],
|
||||
}
|
||||
}, true, customMixin);
|
||||
}
|
||||
|
||||
if (options.xAxisMin) {
|
||||
retval = mixin(retval, {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
time: {
|
||||
min: options.xAxisMin
|
||||
}
|
||||
}],
|
||||
}
|
||||
}, true, customMixin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retval.legend = <Chart.ChartLegendOptions>{
|
||||
position: options.legendPosition as Chart.PositionType,
|
||||
display: options.legendPosition !== LegendPosition.None,
|
||||
labels: {
|
||||
fontColor: foreground
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// these are custom options that will throw compile errors
|
||||
(<any>retval).viewArea = {
|
||||
backgroundColor: background
|
||||
};
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
public set options(options: IInsightOptions) {
|
||||
this._options = options;
|
||||
this.originalType = options.type as ChartType;
|
||||
if (this.options.type === ChartType.TimeSeries) {
|
||||
this.options.type = ChartType.Line;
|
||||
this.options.dataType = DataType.Point;
|
||||
this.options.dataDirection = DataDirection.Horizontal;
|
||||
}
|
||||
this.data = this._data;
|
||||
}
|
||||
|
||||
public get options(): IInsightOptions {
|
||||
return this._options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Following code is pulled from ng2-charting in order to keep the same
|
||||
* color functionality
|
||||
*/
|
||||
|
||||
const defaultColors: Array<Color> = [
|
||||
[255, 99, 132],
|
||||
[54, 162, 235],
|
||||
[255, 206, 86],
|
||||
[231, 233, 237],
|
||||
[75, 192, 192],
|
||||
[151, 187, 205],
|
||||
[220, 220, 220],
|
||||
[247, 70, 74],
|
||||
[70, 191, 189],
|
||||
[253, 180, 92],
|
||||
[148, 159, 177],
|
||||
[77, 83, 96]
|
||||
];
|
||||
|
||||
type Color = [number, number, number];
|
||||
|
||||
interface ILineColor {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
pointBackgroundColor: string;
|
||||
pointBorderColor: string;
|
||||
pointHoverBackgroundColor: string;
|
||||
pointHoverBorderColor: string;
|
||||
}
|
||||
|
||||
interface IBarColor {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
hoverBackgroundColor: string;
|
||||
hoverBorderColor: string;
|
||||
}
|
||||
|
||||
interface IPieColors {
|
||||
backgroundColor: Array<string>;
|
||||
borderColor: Array<string>;
|
||||
pointBackgroundColor: Array<string>;
|
||||
pointBorderColor: Array<string>;
|
||||
pointHoverBackgroundColor: Array<string>;
|
||||
pointHoverBorderColor: Array<string>;
|
||||
}
|
||||
|
||||
interface IPolarAreaColors {
|
||||
backgroundColor: Array<string>;
|
||||
borderColor: Array<string>;
|
||||
hoverBackgroundColor: Array<string>;
|
||||
hoverBorderColor: Array<string>;
|
||||
}
|
||||
|
||||
function rgba(colour: Color, alpha: number): string {
|
||||
return 'rgba(' + colour.concat(alpha).join(',') + ')';
|
||||
}
|
||||
|
||||
function getRandomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function getRandomColor(): Color {
|
||||
return [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors for line|bar charts
|
||||
*/
|
||||
function generateColor(index: number): Color {
|
||||
return defaultColors[index] || getRandomColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors for pie|doughnut charts
|
||||
*/
|
||||
function generateColors(count: number): Array<Color> {
|
||||
const colorsArr = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
colorsArr[i] = defaultColors[i] || getRandomColor();
|
||||
}
|
||||
return colorsArr;
|
||||
}
|
||||
|
||||
function formatLineColor(colors: Color): ILineColor {
|
||||
return {
|
||||
backgroundColor: rgba(colors, 0.4),
|
||||
borderColor: rgba(colors, 1),
|
||||
pointBackgroundColor: rgba(colors, 1),
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: rgba(colors, 0.8)
|
||||
};
|
||||
}
|
||||
|
||||
function formatBarColor(colors: Color): IBarColor {
|
||||
return {
|
||||
backgroundColor: rgba(colors, 0.6),
|
||||
borderColor: rgba(colors, 1),
|
||||
hoverBackgroundColor: rgba(colors, 0.8),
|
||||
hoverBorderColor: rgba(colors, 1)
|
||||
};
|
||||
}
|
||||
|
||||
function formatPieColors(colors: Array<Color>): IPieColors {
|
||||
return {
|
||||
backgroundColor: colors.map(color => rgba(color, 0.6)),
|
||||
borderColor: colors.map(() => '#fff'),
|
||||
pointBackgroundColor: colors.map(color => rgba(color, 1)),
|
||||
pointBorderColor: colors.map(() => '#fff'),
|
||||
pointHoverBackgroundColor: colors.map(color => rgba(color, 1)),
|
||||
pointHoverBorderColor: colors.map(color => rgba(color, 1))
|
||||
};
|
||||
}
|
||||
|
||||
function formatPolarAreaColors(colors: Array<Color>): IPolarAreaColors {
|
||||
return {
|
||||
backgroundColor: colors.map(color => rgba(color, 0.6)),
|
||||
borderColor: colors.map(color => rgba(color, 1)),
|
||||
hoverBackgroundColor: colors.map(color => rgba(color, 0.8)),
|
||||
hoverBorderColor: colors.map(color => rgba(color, 1))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors by chart type
|
||||
*/
|
||||
function getColors(chartType: string, index: number, count: number): Color | ILineColor | IBarColor | IPieColors | IPolarAreaColors {
|
||||
if (chartType === 'pie' || chartType === 'doughnut') {
|
||||
return formatPieColors(generateColors(count));
|
||||
}
|
||||
if (chartType === 'polarArea') {
|
||||
return formatPolarAreaColors(generateColors(count));
|
||||
}
|
||||
if (chartType === 'line' || chartType === 'radar') {
|
||||
return formatLineColor(generateColor(index));
|
||||
}
|
||||
if (chartType === 'bar' || chartType === 'horizontalBar') {
|
||||
return formatBarColor(generateColor(index));
|
||||
}
|
||||
return generateColor(index);
|
||||
}
|
||||
79
src/sql/workbench/contrib/charts/browser/imageInsight.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsight, IInsightData } from './interfaces';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { IInsightOptions, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import * as nls from 'vs/nls';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
|
||||
export interface IConfig extends IInsightOptions {
|
||||
encoding?: string;
|
||||
imageFormat?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
type: InsightType.Image,
|
||||
encoding: 'hex',
|
||||
imageFormat: 'jpeg'
|
||||
};
|
||||
|
||||
export class ImageInsight implements IInsight {
|
||||
|
||||
public static readonly types = [InsightType.Image];
|
||||
public readonly types = ImageInsight.types;
|
||||
|
||||
private _options: IConfig;
|
||||
|
||||
private imageEle: HTMLImageElement;
|
||||
|
||||
constructor(container: HTMLElement, options: IConfig, @INotificationService private _notificationService: INotificationService) {
|
||||
this._options = mixin(options, defaultConfig, false);
|
||||
this.imageEle = $('img');
|
||||
container.appendChild(this.imageEle);
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
||||
}
|
||||
|
||||
set options(config: IConfig) {
|
||||
this._options = mixin(config, defaultConfig, false);
|
||||
}
|
||||
|
||||
get options(): IConfig {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
set data(data: IInsightData) {
|
||||
const that = this;
|
||||
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
|
||||
let img = data.rows[0][0];
|
||||
if (this._options.encoding === 'hex') {
|
||||
img = ImageInsight._hexToBase64(img);
|
||||
}
|
||||
this.imageEle.onerror = function () {
|
||||
this.src = require.toUrl(`./media/images/invalidImage.png`);
|
||||
that._notificationService.error(nls.localize('invalidImage', "Table does not contain a valid image"));
|
||||
};
|
||||
this.imageEle.src = `data:image/${this._options.imageFormat};base64,${img}`;
|
||||
}
|
||||
}
|
||||
|
||||
private static _hexToBase64(hexVal: string) {
|
||||
|
||||
if (startsWith(hexVal, '0x')) {
|
||||
hexVal = hexVal.slice(2);
|
||||
}
|
||||
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
|
||||
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ').map(v => Number(v))));
|
||||
}
|
||||
}
|
||||
103
src/sql/workbench/contrib/charts/browser/insight.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Graph } from './graphInsight';
|
||||
import { ImageInsight } from './imageInsight';
|
||||
import { TableInsight } from './tableInsight';
|
||||
import { IInsight, IInsightCtor, IInsightData } from './interfaces';
|
||||
import { CountInsight } from './countInsight';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Dimension, clearNode } from 'vs/base/browser/dom';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import { IInsightOptions, ChartType, DataDirection, InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
const defaultOptions: IInsightOptions = {
|
||||
type: ChartType.Bar,
|
||||
dataDirection: DataDirection.Horizontal
|
||||
};
|
||||
|
||||
export class Insight {
|
||||
private _insight: IInsight;
|
||||
|
||||
public get insight(): IInsight {
|
||||
return this._insight;
|
||||
}
|
||||
|
||||
private _options: IInsightOptions;
|
||||
private _data: IInsightData;
|
||||
private dim: Dimension;
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement, options: IInsightOptions = defaultOptions,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
this.options = options;
|
||||
this.buildInsight();
|
||||
}
|
||||
|
||||
public layout(dim: Dimension) {
|
||||
this.dim = dim;
|
||||
this.insight.layout(dim);
|
||||
}
|
||||
|
||||
public set options(val: IInsightOptions) {
|
||||
this._options = deepClone(val);
|
||||
if (this.insight) {
|
||||
// check to see if we need to change the insight type
|
||||
if (!find(this.insight.types, x => x === this.options.type)) {
|
||||
this.buildInsight();
|
||||
} else {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get options(): IInsightOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
public set data(val: IInsightData) {
|
||||
this._data = val;
|
||||
if (this.insight) {
|
||||
this.insight.data = val;
|
||||
}
|
||||
}
|
||||
|
||||
private buildInsight() {
|
||||
if (this.insight) {
|
||||
this.insight.dispose();
|
||||
}
|
||||
|
||||
clearNode(this.container);
|
||||
|
||||
let ctor = this.findctor(this.options.type);
|
||||
|
||||
if (ctor) {
|
||||
this._insight = this._instantiationService.createInstance(ctor, this.container, this.options);
|
||||
this.insight.layout(this.dim);
|
||||
if (this._data) {
|
||||
this.insight.data = this._data;
|
||||
}
|
||||
}
|
||||
}
|
||||
public get isCopyable(): boolean {
|
||||
return !!find(Graph.types, x => x === this.options.type as ChartType);
|
||||
}
|
||||
|
||||
private findctor(type: ChartType | InsightType): IInsightCtor {
|
||||
if (find(Graph.types, x => x === type as ChartType)) {
|
||||
return Graph;
|
||||
} else if (find(ImageInsight.types, x => x === type as InsightType)) {
|
||||
return ImageInsight;
|
||||
} else if (find(TableInsight.types, x => x === type as InsightType)) {
|
||||
return TableInsight;
|
||||
} else if (find(CountInsight.types, x => x === type as InsightType)) {
|
||||
return CountInsight;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
51
src/sql/workbench/contrib/charts/browser/interfaces.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { IInsightOptions, InsightType, ChartType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export interface IPointDataSet {
|
||||
data: Array<{ x: number | string, y: number }>;
|
||||
label?: string;
|
||||
fill: boolean;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export function customMixin(destination: any, source: any, overwrite?: boolean): any {
|
||||
if (types.isObject(source)) {
|
||||
mixin(destination, source, overwrite, customMixin);
|
||||
} else if (types.isArray(source)) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (destination[i]) {
|
||||
mixin(destination[i], source[i], overwrite, customMixin);
|
||||
} else {
|
||||
destination[i] = source[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destination = source;
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
export interface IInsightData {
|
||||
columns: Array<string>;
|
||||
rows: Array<Array<string>>;
|
||||
}
|
||||
|
||||
export interface IInsight {
|
||||
options: IInsightOptions;
|
||||
data: IInsightData;
|
||||
readonly types: Array<InsightType | ChartType>;
|
||||
layout(dim: Dimension);
|
||||
dispose();
|
||||
}
|
||||
|
||||
export interface IInsightCtor {
|
||||
new(container: HTMLElement, options: IInsightOptions, ...services: { _serviceBrand: undefined; }[]): IInsight;
|
||||
readonly types: Array<InsightType | ChartType>;
|
||||
}
|
||||
30
src/sql/workbench/contrib/charts/browser/media/chartView.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.chart-parent-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actionbar-container {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.charting-container {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.insight-container {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
width: 250px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.count-label-container {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.label-container {
|
||||
font-size: 20px
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
72
src/sql/workbench/contrib/charts/browser/tableInsight.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsight, IInsightData } from './interfaces';
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
|
||||
import { $, Dimension } from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { InsightType } from 'sql/workbench/contrib/charts/common/interfaces';
|
||||
|
||||
export class TableInsight extends Disposable implements IInsight {
|
||||
public static readonly types = [InsightType.Table];
|
||||
public readonly types = TableInsight.types;
|
||||
|
||||
private table: Table<any>;
|
||||
private dataView: TableDataView<any>;
|
||||
private columns: Slick.Column<any>[];
|
||||
|
||||
constructor(container: HTMLElement, options: any,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
let tableContainer = $('div');
|
||||
tableContainer.style.width = '100%';
|
||||
tableContainer.style.height = '100%';
|
||||
container.appendChild(tableContainer);
|
||||
this.dataView = new TableDataView();
|
||||
this.table = new Table(tableContainer, { dataProvider: this.dataView }, { showRowNumber: true });
|
||||
this.table.setSelectionModel(new CellSelectionModel());
|
||||
this._register(attachTableStyler(this.table, themeService));
|
||||
}
|
||||
|
||||
set data(data: IInsightData) {
|
||||
this.dataView.clear();
|
||||
this.dataView.push(transformData(data.rows, data.columns));
|
||||
this.columns = transformColumns(data.columns);
|
||||
this.table.columns = this.columns;
|
||||
}
|
||||
|
||||
layout(dim: Dimension) {
|
||||
this.table.layout(dim);
|
||||
}
|
||||
|
||||
public options;
|
||||
|
||||
}
|
||||
|
||||
function transformData(rows: string[][], columns: string[]): { [key: string]: string }[] {
|
||||
return rows.map(row => {
|
||||
let object: { [key: string]: string } = {};
|
||||
row.forEach((val, index) => {
|
||||
object[columns[index]] = val;
|
||||
});
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
return columns.map(col => {
|
||||
return <Slick.Column<any>>{
|
||||
name: col,
|
||||
id: col,
|
||||
field: col
|
||||
};
|
||||
});
|
||||
}
|
||||
66
src/sql/workbench/contrib/charts/common/interfaces.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class ChartState {
|
||||
dataId: { batchId: number, resultId: number };
|
||||
options: IInsightOptions = {
|
||||
type: ChartType.Bar
|
||||
};
|
||||
|
||||
dispose() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface IInsightOptions {
|
||||
type: InsightType | ChartType;
|
||||
dataDirection?: DataDirection;
|
||||
dataType?: DataType;
|
||||
labelFirstColumn?: boolean;
|
||||
columnsAsLabels?: boolean;
|
||||
legendPosition?: LegendPosition;
|
||||
yAxisLabel?: string;
|
||||
yAxisMin?: number;
|
||||
yAxisMax?: number;
|
||||
xAxisLabel?: string;
|
||||
xAxisMin?: number;
|
||||
xAxisMax?: number;
|
||||
encoding?: string;
|
||||
imageFormat?: string;
|
||||
}
|
||||
|
||||
export enum InsightType {
|
||||
Image = 'image',
|
||||
Table = 'table',
|
||||
Count = 'count'
|
||||
}
|
||||
|
||||
export enum ChartType {
|
||||
Bar = 'bar',
|
||||
Doughnut = 'doughnut',
|
||||
HorizontalBar = 'horizontalBar',
|
||||
Line = 'line',
|
||||
Pie = 'pie',
|
||||
TimeSeries = 'timeSeries',
|
||||
Scatter = 'scatter'
|
||||
}
|
||||
|
||||
export enum LegendPosition {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export enum DataType {
|
||||
Number = 'number',
|
||||
Point = 'point'
|
||||
}
|
||||
|
||||
export enum DataDirection {
|
||||
Vertical = 'vertical',
|
||||
Horizontal = 'horizontal'
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ChartView } from 'sql/workbench/contrib/charts/browser/chartView';
|
||||
import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService';
|
||||
import { TestLayoutService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
suite('Chart View', () => {
|
||||
test('initializes without error', () => {
|
||||
const chartview = createChartView();
|
||||
assert(chartview);
|
||||
});
|
||||
|
||||
test('renders without error', () => {
|
||||
const chartview = createChartView();
|
||||
chartview.render(document.createElement('div'));
|
||||
});
|
||||
});
|
||||
|
||||
function createChartView(): ChartView {
|
||||
const layoutService = new TestLayoutService();
|
||||
const contextViewService = new ContextViewService(layoutService);
|
||||
const themeService = new TestThemeService();
|
||||
const instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(IThemeService, themeService);
|
||||
return new ChartView(contextViewService, themeService, instantiationService);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { CommandLineWorkbenchContribution } from 'sql/workbench/contrib/commandLine/electron-browser/commandLine';
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CommandLineWorkbenchContribution, LifecyclePhase.Restored);
|
||||
@@ -0,0 +1,261 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as querystring from 'querystring';
|
||||
import * as azdata from 'azdata';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
||||
import { equalsIgnoreCase } from 'vs/base/common/strings';
|
||||
import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import * as platform from 'vs/platform/registry/common/platform';
|
||||
import { IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/contrib/connection/common/connectionProviderExtension';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { localize } from 'vs/nls';
|
||||
import { QueryInput } from 'sql/workbench/contrib/query/common/queryInput';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { openNewQuery } from 'sql/workbench/contrib/query/browser/queryActions';
|
||||
import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
const connectAuthority = 'connect';
|
||||
|
||||
interface SqlArgs {
|
||||
_?: string[];
|
||||
aad?: boolean;
|
||||
database?: string;
|
||||
integrated?: boolean;
|
||||
server?: string;
|
||||
user?: string;
|
||||
command?: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export class CommandLineWorkbenchContribution implements IWorkbenchContribution, IURLHandler {
|
||||
|
||||
constructor(
|
||||
@ICapabilitiesService private readonly _capabilitiesService: ICapabilitiesService,
|
||||
@IConnectionManagementService private readonly _connectionManagementService: IConnectionManagementService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IURLService urlService: IURLService,
|
||||
@IDialogService private readonly dialogService: IDialogService
|
||||
) {
|
||||
if (ipc) {
|
||||
ipc.on('ads:processCommandLine', (event: any, args: ParsedArgs) => this.onLaunched(args));
|
||||
}
|
||||
// we only get the ipc from main during window reuse
|
||||
if (environmentService) {
|
||||
this.onLaunched(environmentService.args);
|
||||
}
|
||||
if (urlService) {
|
||||
urlService.registerHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
private onLaunched(args: ParsedArgs) {
|
||||
const registry = platform.Registry.as<IConnectionProviderRegistry>(ConnectionProviderExtensions.ConnectionProviderContributions);
|
||||
let sqlProvider = registry.getProperties(Constants.mssqlProviderName);
|
||||
// We can't connect to object explorer until the MSSQL connection provider is registered
|
||||
if (sqlProvider) {
|
||||
this.processCommandLine(args).catch(reason => { this.logService.warn('processCommandLine failed: ' + reason); });
|
||||
} else {
|
||||
registry.onNewProvider(e => {
|
||||
if (e.id === Constants.mssqlProviderName) {
|
||||
this.processCommandLine(args).catch(reason => { this.logService.warn('processCommandLine failed: ' + reason); });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We base our logic on the combination of (server, command) values.
|
||||
// (serverName, commandName) => Connect object explorer and execute the command, passing the connection profile to the command. Do not load query editor.
|
||||
// (null, commandName) => Launch the command with a null connection. If the command implementation needs a connection, it will need to create it.
|
||||
// (serverName, null) => Connect object explorer and open a new query editor if no file names are passed. If file names are passed, connect their editors to the server.
|
||||
// (null, null) => Prompt for a connection unless there are registered servers
|
||||
public async processCommandLine(args: SqlArgs): Promise<void> {
|
||||
let profile: IConnectionProfile = undefined;
|
||||
let commandName = undefined;
|
||||
if (args) {
|
||||
if (this._commandService) {
|
||||
commandName = args.command;
|
||||
}
|
||||
|
||||
if (args.server) {
|
||||
profile = this.readProfileFromArgs(args);
|
||||
}
|
||||
}
|
||||
let showConnectDialogOnStartup: boolean = this._configurationService.getValue('workbench.showConnectDialogOnStartup');
|
||||
if (showConnectDialogOnStartup && !commandName && !profile && !this._connectionManagementService.hasRegisteredServers()) {
|
||||
// prompt the user for a new connection on startup if no profiles are registered
|
||||
await this._connectionManagementService.showConnectionDialog();
|
||||
return;
|
||||
}
|
||||
let connectedContext: azdata.ConnectedContext = undefined;
|
||||
if (profile) {
|
||||
if (this._notificationService) {
|
||||
this._notificationService.status(localize('connectingLabel', "Connecting: {0}", profile.serverName), { hideAfter: 2500 });
|
||||
}
|
||||
try {
|
||||
await this._connectionManagementService.connectIfNotConnected(profile, 'connection', true);
|
||||
// Before sending to extensions, we should a) serialize to IConnectionProfile or things will fail,
|
||||
// and b) use the latest version of the profile from the service so most fields are filled in.
|
||||
let updatedProfile = this._connectionManagementService.getConnectionProfileById(profile.id);
|
||||
connectedContext = { connectionProfile: new ConnectionProfile(this._capabilitiesService, updatedProfile).toIConnectionProfile() };
|
||||
} catch (err) {
|
||||
this.logService.warn('Failed to connect due to error' + getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
if (commandName) {
|
||||
if (this._notificationService) {
|
||||
this._notificationService.status(localize('runningCommandLabel', "Running command: {0}", commandName), { hideAfter: 2500 });
|
||||
}
|
||||
await this._commandService.executeCommand(commandName, connectedContext);
|
||||
} else if (profile) {
|
||||
// If we were given a file and it was opened with the sql editor,
|
||||
// we want to connect the given profile to to it.
|
||||
// If more than one file was passed, only show the connection dialog error on one of them.
|
||||
if (args._ && args._.length > 0) {
|
||||
await Promise.all(args._.map((f, i) => this.processFile(URI.file(f).toString(), profile, i === 0)));
|
||||
}
|
||||
else {
|
||||
// Default to showing new query
|
||||
if (this._notificationService) {
|
||||
this._notificationService.status(localize('openingNewQueryLabel', "Opening new query: {0}", profile.serverName), { hideAfter: 2500 });
|
||||
}
|
||||
try {
|
||||
await this.instantiationService.invokeFunction(openNewQuery, profile);
|
||||
} catch (error) {
|
||||
this.logService.warn('unable to open query editor ' + error);
|
||||
// Note: we are intentionally swallowing this error.
|
||||
// In part this is to accommodate unit testing where we don't want to set up the query stack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async handleURL(uri: URI): Promise<boolean> {
|
||||
// Catch file URLs
|
||||
let authority = uri.authority.toLowerCase();
|
||||
if (authority === connectAuthority) {
|
||||
try {
|
||||
let args = this.parseProtocolArgs(uri);
|
||||
if (!args.server) {
|
||||
this._notificationService.warn(localize('warnServerRequired', "Cannot connect as no server information was provided"));
|
||||
return true;
|
||||
}
|
||||
let isOpenOk = await this.confirmConnect(args);
|
||||
if (isOpenOk) {
|
||||
await this.processCommandLine(args);
|
||||
}
|
||||
} catch (err) {
|
||||
this._notificationService.error(localize('errConnectUrl', "Could not open URL due to error {0}", getErrorMessage(err)));
|
||||
}
|
||||
// Handled either way
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async confirmConnect(args: SqlArgs): Promise<boolean> {
|
||||
let detail = args && args.server ? localize('connectServerDetail', "This will connect to server {0}", args.server) : '';
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('confirmConnect', "Are you sure you want to connect?"),
|
||||
detail: detail,
|
||||
primaryButton: localize('open', "&&Open"),
|
||||
type: 'question'
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private parseProtocolArgs(uri: URI): SqlArgs {
|
||||
let args: SqlArgs = querystring.parse(uri.query);
|
||||
// Clear out command, not supporting arbitrary command via this path
|
||||
args.command = undefined;
|
||||
return args;
|
||||
}
|
||||
|
||||
// If an open and connectable query editor exists for the given URI, attach it to the connection profile
|
||||
private async processFile(uriString: string, profile: IConnectionProfile, warnOnConnectFailure: boolean): Promise<void> {
|
||||
let activeEditor = this._editorService.editors.filter(v => v.getResource().toString() === uriString).pop();
|
||||
if (activeEditor) {
|
||||
let queryInput = activeEditor as QueryInput;
|
||||
if (queryInput && queryInput.state.connected) {
|
||||
let options: IConnectionCompletionOptions = {
|
||||
params: { connectionType: ConnectionType.editor, runQueryOnCompletion: RunQueryOnConnectionMode.none, input: queryInput },
|
||||
saveTheConnection: false,
|
||||
showDashboard: false,
|
||||
showConnectionDialogOnError: warnOnConnectFailure,
|
||||
showFirewallRuleOnError: warnOnConnectFailure
|
||||
};
|
||||
if (this._notificationService) {
|
||||
this._notificationService.status(localize('connectingQueryLabel', "Connecting query file"), { hideAfter: 2500 });
|
||||
}
|
||||
await this._connectionManagementService.connect(profile, uriString, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readProfileFromArgs(args: SqlArgs) {
|
||||
let profile = new ConnectionProfile(this._capabilitiesService, null);
|
||||
// We want connection store to use any matching password it finds
|
||||
profile.savePassword = true;
|
||||
profile.providerName = args.provider ? args.provider : Constants.mssqlProviderName;
|
||||
profile.serverName = args.server;
|
||||
profile.databaseName = args.database ? args.database : '';
|
||||
profile.userName = args.user ? args.user : '';
|
||||
profile.authenticationType = args.integrated ? Constants.integrated : args.aad ? Constants.azureMFA : (profile.userName.length > 0) ? Constants.sqlLogin : Constants.integrated;
|
||||
profile.connectionName = '';
|
||||
profile.setOptionValue('applicationName', Constants.applicationName);
|
||||
profile.setOptionValue('databaseDisplayName', profile.databaseName);
|
||||
profile.setOptionValue('groupId', profile.groupId);
|
||||
return this._connectionManagementService ? this.tryMatchSavedProfile(profile) : profile;
|
||||
}
|
||||
|
||||
private tryMatchSavedProfile(profile: ConnectionProfile) {
|
||||
let match: ConnectionProfile = undefined;
|
||||
// If we can find a saved mssql provider connection that matches the args, use it
|
||||
let groups = this._connectionManagementService.getConnectionGroups([Constants.mssqlProviderName]);
|
||||
if (groups && groups.length > 0) {
|
||||
let rootGroup = groups[0];
|
||||
let connections = ConnectionProfileGroup.getConnectionsInGroup(rootGroup);
|
||||
match = find(connections, (c) => this.matchProfile(profile, c));
|
||||
}
|
||||
return match ? match : profile;
|
||||
}
|
||||
|
||||
// determines if the 2 profiles are a functional match
|
||||
// profile1 is the profile generated from command line parameters
|
||||
private matchProfile(profile1: ConnectionProfile, profile2: ConnectionProfile): boolean {
|
||||
return equalsIgnoreCase(profile1.serverName, profile2.serverName)
|
||||
&& equalsIgnoreCase(profile1.providerName, profile2.providerName)
|
||||
// case sensitive servers can have 2 databases whose name differs only in case
|
||||
&& profile1.databaseName === profile2.databaseName
|
||||
&& equalsIgnoreCase(profile1.userName, profile2.userName)
|
||||
&& profile1.authenticationType === profile2.authenticationType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as azdata from 'azdata';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
||||
import { CommandLineWorkbenchContribution } from 'sql/workbench/contrib/commandLine/electron-browser/commandLine';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import { ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { TestCommandService } from 'vs/editor/test/browser/editorTestServices';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { assertThrowsAsync } from 'sql/base/test/common/async';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestEditorService, TestDialogService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { QueryInput, QueryEditorState } from 'sql/workbench/contrib/query/common/queryInput';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
|
||||
class TestParsedArgs implements ParsedArgs {
|
||||
[arg: string]: any;
|
||||
_: string[];
|
||||
aad?: boolean;
|
||||
add?: boolean;
|
||||
database?: string;
|
||||
command?: string;
|
||||
debugBrkPluginHost?: string;
|
||||
debugBrkSearch?: string;
|
||||
debugId?: string;
|
||||
debugPluginHost?: string;
|
||||
debugSearch?: string;
|
||||
diff?: boolean;
|
||||
'disable-crash-reporter'?: boolean;
|
||||
'disable-extension'?: string[]; // undefined or array of 1 or more
|
||||
'disable-extensions'?: boolean;
|
||||
'disable-restore-windows'?: boolean;
|
||||
'disable-telemetry'?: boolean;
|
||||
'disable-updates'?: boolean;
|
||||
'driver'?: string;
|
||||
'enable-proposed-api'?: string[];
|
||||
'export-default-configuration'?: string;
|
||||
'extensions-dir'?: string;
|
||||
extensionDevelopmentPath?: string[];
|
||||
extensionTestsPath?: string;
|
||||
'file-chmod'?: boolean;
|
||||
'file-write'?: boolean;
|
||||
'folder-uri'?: string[];
|
||||
goto?: boolean;
|
||||
help?: boolean;
|
||||
'install-extension'?: string[];
|
||||
'install-source'?: string;
|
||||
integrated?: boolean;
|
||||
'list-extensions'?: boolean;
|
||||
locale?: string;
|
||||
log?: string;
|
||||
logExtensionHostCommunication?: boolean;
|
||||
'max-memory'?: string;
|
||||
'new-window'?: boolean;
|
||||
'open-url'?: boolean;
|
||||
performance?: boolean;
|
||||
'prof-append-timers'?: string;
|
||||
'prof-startup'?: boolean;
|
||||
'prof-startup-prefix'?: string;
|
||||
'reuse-window'?: boolean;
|
||||
server?: string;
|
||||
'show-versions'?: boolean;
|
||||
'skip-add-to-recently-opened'?: boolean;
|
||||
'skip-getting-started'?: boolean;
|
||||
'skip-release-notes'?: boolean;
|
||||
status?: boolean;
|
||||
'sticky-quickopen'?: boolean;
|
||||
'uninstall-extension'?: string[];
|
||||
'unity-launch'?: boolean; // Always open a new window, except if opening the first window or opening a file or folder as part of the launch.
|
||||
'upload-logs'?: string;
|
||||
user?: string;
|
||||
'user-data-dir'?: string;
|
||||
_urls?: string[];
|
||||
verbose?: boolean;
|
||||
version?: boolean;
|
||||
wait?: boolean;
|
||||
waitMarkerFilePath?: string;
|
||||
}
|
||||
suite('commandLineService tests', () => {
|
||||
|
||||
let capabilitiesService: TestCapabilitiesService;
|
||||
setup(() => {
|
||||
capabilitiesService = new TestCapabilitiesService();
|
||||
});
|
||||
|
||||
function getCommandLineContribution(
|
||||
connectionManagementService: IConnectionManagementService,
|
||||
configurationService: IConfigurationService,
|
||||
capabilitiesService?: ICapabilitiesService,
|
||||
commandService?: ICommandService,
|
||||
editorService?: IEditorService,
|
||||
logService?: ILogService,
|
||||
dialogService?: IDialogService,
|
||||
notificationService?: INotificationService
|
||||
): CommandLineWorkbenchContribution {
|
||||
return new CommandLineWorkbenchContribution(
|
||||
capabilitiesService,
|
||||
connectionManagementService,
|
||||
undefined,
|
||||
editorService,
|
||||
commandService,
|
||||
configurationService,
|
||||
notificationService,
|
||||
logService,
|
||||
undefined,
|
||||
undefined,
|
||||
dialogService
|
||||
);
|
||||
}
|
||||
|
||||
function getConfigurationServiceMock(showConnectDialogOnStartup: boolean): TypeMoq.Mock<IConfigurationService> {
|
||||
let configurationService = TypeMoq.Mock.ofType<IConfigurationService>(TestConfigurationService);
|
||||
configurationService.setup((c) => c.getValue(TypeMoq.It.isAnyString())).returns((config: string) => showConnectDialogOnStartup);
|
||||
return configurationService;
|
||||
}
|
||||
|
||||
test('processCommandLine shows connection dialog by default', done => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog())
|
||||
.returns(() => new Promise<void>((resolve, reject) => { resolve(); }))
|
||||
.verifiable();
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object);
|
||||
contribution.processCommandLine(new TestParsedArgs()).then(() => {
|
||||
connectionManagementService.verifyAll();
|
||||
done();
|
||||
}, error => { assert.fail(error, null, 'processCommandLine rejected ' + error); done(); });
|
||||
});
|
||||
|
||||
test('processCommandLine does nothing if no server name and command name is provided and the configuration \'workbench.showConnectDialogOnStartup\' is set to false, even if registered servers exist', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => false);
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
const configurationService = getConfigurationServiceMock(false);
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object);
|
||||
|
||||
await contribution.processCommandLine(new TestParsedArgs());
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
test('processCommandLine does nothing if registered servers exist and no server name is provided', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.returns(() => new Promise<string>((resolve, reject) => { resolve('unused'); }))
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object);
|
||||
try {
|
||||
await contribution.processCommandLine(new TestParsedArgs());
|
||||
connectionManagementService.verifyAll();
|
||||
} catch (error) {
|
||||
assert.fail(error, null, 'processCommandLine rejected ' + error);
|
||||
}
|
||||
});
|
||||
|
||||
test('processCommandLine opens a new connection if a server name is passed', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
args.server = 'myserver';
|
||||
args.database = 'mydatabase';
|
||||
args.user = 'myuser';
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const logService = new NullLogService();
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService);
|
||||
await contribution.processCommandLine(args);
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
test('processCommandLine invokes a command without a profile parameter when no server is passed', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Loose);
|
||||
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
|
||||
args.command = 'mycommand';
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
let capturedArgs: any;
|
||||
commandService.setup(c => c.executeCommand(TypeMoq.It.isAnyString(), undefined))
|
||||
.returns((command, args) => {
|
||||
capturedArgs = args;
|
||||
return Promise.resolve();
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object);
|
||||
await contribution.processCommandLine(args);
|
||||
connectionManagementService.verifyAll();
|
||||
commandService.verifyAll();
|
||||
assert(isUndefinedOrNull(capturedArgs));
|
||||
});
|
||||
|
||||
|
||||
test('processCommandLine invokes a command with a profile parameter when a server is passed', async () => {
|
||||
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
args.command = 'mycommand';
|
||||
args.server = 'myserver';
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver'), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
let actualProfile: azdata.ConnectedContext = undefined;
|
||||
commandService.setup(c => c.executeCommand('mycommand', TypeMoq.It.isAny()))
|
||||
.returns((cmdName, profile) => {
|
||||
actualProfile = profile;
|
||||
return Promise.resolve();
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object);
|
||||
await contribution.processCommandLine(args);
|
||||
connectionManagementService.verifyAll();
|
||||
commandService.verifyAll();
|
||||
assert(!isUndefinedOrNull(actualProfile));
|
||||
assert.equal(actualProfile.connectionProfile.serverName, args.server);
|
||||
|
||||
});
|
||||
|
||||
test('processCommandLine rejects unknown commands', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
|
||||
args.command = 'mycommand';
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
|
||||
commandService.setup(c => c.executeCommand('mycommand'))
|
||||
.returns(() => Promise.reject(new Error('myerror')))
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object);
|
||||
assertThrowsAsync(async () => await contribution.processCommandLine(args));
|
||||
});
|
||||
|
||||
test('processCommandLine uses Integrated auth if no user name or auth type is passed', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
args.server = 'myserver';
|
||||
args.database = 'mydatabase';
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.integrated), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const logService = new NullLogService();
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService);
|
||||
await contribution.processCommandLine(args);
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
test('processCommandLine reuses saved connections that match args', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
let connection = new ConnectionProfile(capabilitiesService, {
|
||||
connectionName: 'Test',
|
||||
savePassword: false,
|
||||
groupFullName: 'testGroup',
|
||||
serverName: 'myserver',
|
||||
databaseName: 'mydatabase',
|
||||
authenticationType: Constants.integrated,
|
||||
password: undefined,
|
||||
userName: '',
|
||||
groupId: undefined,
|
||||
providerName: 'MSSQL',
|
||||
options: {},
|
||||
saveProfile: true,
|
||||
id: 'testID'
|
||||
});
|
||||
let conProfGroup = new ConnectionProfileGroup('testGroup', undefined, 'testGroup', undefined, undefined);
|
||||
conProfGroup.connections = [connection];
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
args.server = 'myserver';
|
||||
args.database = 'mydatabase';
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(
|
||||
TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.integrated && p.connectionName === 'Test' && p.id === 'testID'), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById('testID')).returns(() => originalProfile).verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(x => x.getConnectionGroups(TypeMoq.It.isAny())).returns(() => [conProfGroup]);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const logService = new NullLogService();
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService);
|
||||
await contribution.processCommandLine(args);
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
test('processCommandLine connects opened query files to given server', async () => {
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
const args: TestParsedArgs = new TestParsedArgs();
|
||||
args.server = 'myserver';
|
||||
args.database = 'mydatabase';
|
||||
args.user = 'myuser';
|
||||
args._ = ['c:\\dir\\file.sql'];
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
}).verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const queryInput: TypeMoq.Mock<QueryInput> = TypeMoq.Mock.ofType<QueryInput>(QueryInput);
|
||||
let uri = URI.file(args._[0]);
|
||||
const queryState = new QueryEditorState();
|
||||
queryState.connected = true;
|
||||
queryInput.setup(q => q.state).returns(() => queryState);
|
||||
queryInput.setup(q => q.getResource()).returns(() => uri).verifiable(TypeMoq.Times.once());
|
||||
const editorService: TypeMoq.Mock<IEditorService> = TypeMoq.Mock.ofType<IEditorService>(TestEditorService, TypeMoq.MockBehavior.Strict);
|
||||
editorService.setup(e => e.editors).returns(() => [queryInput.object]);
|
||||
connectionManagementService.setup(c =>
|
||||
c.connect(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin),
|
||||
uri.toString(),
|
||||
TypeMoq.It.is<IConnectionCompletionOptions>(i => i.params.input === queryInput.object && i.params.connectionType === ConnectionType.editor))
|
||||
).verifiable(TypeMoq.Times.once());
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, editorService.object);
|
||||
await contribution.processCommandLine(args);
|
||||
queryInput.verifyAll();
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
suite('URL Handler', () => {
|
||||
|
||||
let dialogService: TypeMoq.Mock<TestDialogService>;
|
||||
|
||||
setup(() => {
|
||||
dialogService = TypeMoq.Mock.ofType(TestDialogService);
|
||||
});
|
||||
|
||||
|
||||
test('handleUrl ignores non-connect URLs', async () => {
|
||||
// Given a URI pointing to a server
|
||||
let uri: URI = URI.parse('azuredatastudio://file?server=myserver&database=mydatabase&user=myuser');
|
||||
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const logService = new NullLogService();
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService, dialogService.object);
|
||||
|
||||
// When I call the URL handler and user confirms they should connect
|
||||
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: true }));
|
||||
let result = await contribution.handleURL(uri);
|
||||
|
||||
// Then I expect connection management service to have been called
|
||||
assert.equal(result, false, 'Expected URL to be ignored');
|
||||
});
|
||||
|
||||
test('handleUrl opens a new connection if a server name is passed', async () => {
|
||||
// Given a URI pointing to a server
|
||||
let uri: URI = URI.parse('azuredatastudio://connect?server=myserver&database=mydatabase&user=myuser');
|
||||
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const logService = new NullLogService();
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService, dialogService.object);
|
||||
|
||||
// When I call the URL handler and user confirms they should connect
|
||||
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: true }));
|
||||
let result = await contribution.handleURL(uri);
|
||||
|
||||
// Then I expect connection management service to have been called
|
||||
assert.equal(result, true, 'Expected URL to be handled');
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
test('handleUrl does nothing if a user does not confirm', async () => {
|
||||
// Given a URI pointing to a server
|
||||
let uri: URI = URI.parse('azuredatastudio://connect?server=myserver&database=mydatabase&user=myuser');
|
||||
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
let originalProfile: IConnectionProfile = undefined;
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
|
||||
.returns((conn) => {
|
||||
originalProfile = conn;
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
// Note: should not run since we expect to cancel before this
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile);
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
const logService = new NullLogService();
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService, dialogService.object);
|
||||
|
||||
// When I call the URL handler and user says no on confirmation dialog
|
||||
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: false }));
|
||||
let result = await contribution.handleURL(uri);
|
||||
|
||||
// Then I expect no connection, but the URL should still be handled
|
||||
assert.equal(result, true, 'Expected URL to be handled');
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
test('handleUrl ignores commands', async () => {
|
||||
// Given I pass a command
|
||||
let uri: URI = URI.parse('azuredatastudio://connect?command=mycommand');
|
||||
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
|
||||
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true);
|
||||
commandService.setup(c => c.executeCommand('mycommand'))
|
||||
.returns(() => Promise.resolve())
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
|
||||
const notificationService = TypeMoq.Mock.ofType(TestNotificationService);
|
||||
notificationService.setup(n => n.warn(TypeMoq.It.isAny())).returns(() => undefined)
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object, undefined, new NullLogService(), dialogService.object, notificationService.object);
|
||||
|
||||
// When I handle the command URL
|
||||
let result = await contribution.handleURL(uri);
|
||||
|
||||
// Then command service should not have been called, and instead connection should be handled
|
||||
assert.equal(result, true);
|
||||
commandService.verifyAll();
|
||||
notificationService.verifyAll();
|
||||
});
|
||||
|
||||
test('handleUrl ignores commands and connects', async () => {
|
||||
// Given I pass a command
|
||||
let uri: URI = URI.parse('azuredatastudio://connect?command=mycommand&server=myserver&database=mydatabase&user=myuser');
|
||||
|
||||
const connectionManagementService: TypeMoq.Mock<IConnectionManagementService>
|
||||
= TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService, TypeMoq.MockBehavior.Strict);
|
||||
const commandService: TypeMoq.Mock<ICommandService> = TypeMoq.Mock.ofType<ICommandService>(TestCommandService);
|
||||
|
||||
connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never());
|
||||
connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce());
|
||||
connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []);
|
||||
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is<ConnectionProfile>(p => p.serverName === 'myserver' && p.authenticationType === Constants.sqlLogin), 'connection', true))
|
||||
.returns((conn) => {
|
||||
return Promise.resolve('unused');
|
||||
})
|
||||
.verifiable(TypeMoq.Times.once());
|
||||
|
||||
commandService.setup(c => c.executeCommand('mycommand'))
|
||||
.returns(() => Promise.resolve())
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
const configurationService = getConfigurationServiceMock(true);
|
||||
|
||||
const notificationService = TypeMoq.Mock.ofType(TestNotificationService);
|
||||
notificationService.setup(n => n.warn(TypeMoq.It.isAny())).returns(() => undefined)
|
||||
.verifiable(TypeMoq.Times.never());
|
||||
let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, commandService.object, undefined, new NullLogService(), dialogService.object, notificationService.object);
|
||||
|
||||
// When I handle the command URL
|
||||
dialogService.setup(d => d.confirm(TypeMoq.It.isAny())).returns(() => Promise.resolve({ confirmed: true }));
|
||||
let result = await contribution.handleURL(uri);
|
||||
|
||||
// Then command service should not have been called, and instead connection should be handled
|
||||
assert.equal(result, true);
|
||||
commandService.verifyAll();
|
||||
notificationService.verifyAll();
|
||||
connectionManagementService.verifyAll();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OptionsDialog } from 'sql/workbench/browser/modal/optionsDialog';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as azdata from 'azdata';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
|
||||
export class AdvancedPropertiesController {
|
||||
private _advancedDialog: OptionsDialog;
|
||||
private _options: { [name: string]: any };
|
||||
|
||||
constructor(private _onCloseAdvancedProperties: () => void,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
private handleOnOk(): void {
|
||||
this._options = this._advancedDialog.optionValues;
|
||||
}
|
||||
|
||||
public showDialog(providerOptions: azdata.ConnectionOption[], options: { [name: string]: any }): void {
|
||||
this._options = options;
|
||||
let serviceOptions = providerOptions.map(option => AdvancedPropertiesController.connectionOptionToServiceOption(option));
|
||||
this.advancedDialog.open(serviceOptions, this._options);
|
||||
}
|
||||
|
||||
public get advancedDialog() {
|
||||
if (!this._advancedDialog) {
|
||||
this._advancedDialog = this._instantiationService.createInstance(
|
||||
OptionsDialog, localize('connectionAdvancedProperties', "Advanced Properties"), TelemetryKeys.ConnectionAdvancedProperties, { hasBackButton: true, cancelLabel: localize('advancedProperties.discard', "Discard") });
|
||||
this._advancedDialog.onCloseEvent(() => this._onCloseAdvancedProperties());
|
||||
this._advancedDialog.onOk(() => this.handleOnOk());
|
||||
this._advancedDialog.render();
|
||||
}
|
||||
return this._advancedDialog;
|
||||
}
|
||||
|
||||
public set advancedDialog(dialog: OptionsDialog) {
|
||||
this._advancedDialog = dialog;
|
||||
}
|
||||
|
||||
public static connectionOptionToServiceOption(connectionOption: azdata.ConnectionOption): azdata.ServiceOption {
|
||||
return {
|
||||
name: connectionOption.name,
|
||||
displayName: connectionOption.displayName,
|
||||
description: connectionOption.description,
|
||||
groupName: connectionOption.groupName,
|
||||
valueType: connectionOption.valueType,
|
||||
defaultValue: connectionOption.defaultValue,
|
||||
objectType: undefined,
|
||||
categoryValues: connectionOption.categoryValues,
|
||||
isRequired: connectionOption.isRequired,
|
||||
isArray: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { AddServerGroupAction, AddServerAction } from 'sql/workbench/contrib/objectExplorer/browser/connectionTreeAction';
|
||||
import { ClearRecentConnectionsAction, GetCurrentConnectionStringAction } from 'sql/workbench/contrib/connection/browser/connectionActions';
|
||||
import * as azdata from 'azdata';
|
||||
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ConnectionStatusbarItem } from 'sql/workbench/contrib/connection/browser/connectionStatus';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { integrated, azureMFA } from 'sql/platform/connection/common/constants';
|
||||
import { AuthenticationType } from 'sql/workbench/services/connection/browser/connectionWidget';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
|
||||
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
|
||||
workbenchRegistry.registerWorkbenchContribution(ConnectionStatusbarItem, LifecyclePhase.Restored);
|
||||
|
||||
// Connection Dashboard registration
|
||||
|
||||
const actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
|
||||
|
||||
// Connection Actions
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
ClearRecentConnectionsAction,
|
||||
ClearRecentConnectionsAction.ID,
|
||||
ClearRecentConnectionsAction.LABEL
|
||||
),
|
||||
ClearRecentConnectionsAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
AddServerGroupAction,
|
||||
AddServerGroupAction.ID,
|
||||
AddServerGroupAction.LABEL
|
||||
),
|
||||
AddServerGroupAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
AddServerAction,
|
||||
AddServerAction.ID,
|
||||
AddServerAction.LABEL
|
||||
),
|
||||
AddServerAction.LABEL
|
||||
);
|
||||
|
||||
CommandsRegistry.registerCommand('azdata.connect',
|
||||
function (accessor, args: {
|
||||
serverName: string,
|
||||
providerName: string,
|
||||
authenticationType?: AuthenticationType,
|
||||
userName?: string,
|
||||
password?: string,
|
||||
databaseName?: string
|
||||
}) {
|
||||
const capabilitiesServices = accessor.get(ICapabilitiesService);
|
||||
const connectionManagementService = accessor.get(IConnectionManagementService);
|
||||
if (args && args.serverName && args.providerName
|
||||
&& (args.authenticationType === integrated
|
||||
|| args.authenticationType === azureMFA
|
||||
|| (args.userName && args.password))) {
|
||||
const profile: azdata.IConnectionProfile = {
|
||||
serverName: args.serverName,
|
||||
databaseName: args.databaseName,
|
||||
authenticationType: args.authenticationType,
|
||||
providerName: args.providerName,
|
||||
connectionName: '',
|
||||
userName: args.userName,
|
||||
password: args.password,
|
||||
savePassword: true,
|
||||
groupFullName: undefined,
|
||||
saveProfile: true,
|
||||
id: undefined,
|
||||
groupId: undefined,
|
||||
options: {}
|
||||
};
|
||||
const connectionProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesServices, profile);
|
||||
|
||||
connectionManagementService.connect(connectionProfile, undefined, {
|
||||
saveTheConnection: true,
|
||||
showDashboard: true,
|
||||
params: undefined,
|
||||
showConnectionDialogOnError: true,
|
||||
showFirewallRuleOnError: true
|
||||
});
|
||||
} else {
|
||||
connectionManagementService.showConnectionDialog();
|
||||
}
|
||||
});
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
GetCurrentConnectionStringAction,
|
||||
GetCurrentConnectionStringAction.ID,
|
||||
GetCurrentConnectionStringAction.LABEL
|
||||
),
|
||||
GetCurrentConnectionStringAction.LABEL
|
||||
);
|
||||
|
||||
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'connection',
|
||||
'title': 'Connection',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'sql.maxRecentConnections': {
|
||||
'type': 'number',
|
||||
'default': 25,
|
||||
'description': localize('sql.maxRecentConnectionsDescription', "The maximum number of recently used connections to store in the connection list.")
|
||||
},
|
||||
'sql.defaultEngine': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.defaultEngineDescription', "Default SQL Engine to use. This drives default language provider in .sql files and the default to use when creating a new connection. Valid option is currently MSSQL"),
|
||||
'default': 'MSSQL'
|
||||
},
|
||||
'connection.parseClipboardForConnectionString': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('connection.parseClipboardForConnectionStringDescription', "Attempt to parse the contents of the clipboard when the connection dialog is opened or a paste is performed.")
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import nls = require('vs/nls');
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { INotificationService, INotificationActions } from 'vs/platform/notification/common/notification';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { QueryInput } from 'sql/workbench/contrib/query/common/queryInput';
|
||||
import { EditDataInput } from 'sql/workbench/contrib/editData/browser/editDataInput';
|
||||
import { DashboardInput } from 'sql/workbench/contrib/dashboard/browser/dashboardInput';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
/**
|
||||
* Workbench action to clear the recent connnections list
|
||||
*/
|
||||
export class ClearRecentConnectionsAction extends Action {
|
||||
|
||||
public static ID = 'clearRecentConnectionsAction';
|
||||
public static LABEL = nls.localize('ClearRecentlyUsedLabel', "Clear List");
|
||||
public static ICON = 'search-action clear-search-results';
|
||||
|
||||
private _onRecentConnectionsRemoved = new Emitter<void>();
|
||||
public onRecentConnectionsRemoved: Event<void> = this._onRecentConnectionsRemoved.event;
|
||||
|
||||
private _useConfirmationMessage = false;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IQuickInputService private _quickInputService: IQuickInputService,
|
||||
@IDialogService private _dialogService: IDialogService,
|
||||
) {
|
||||
super(id, label, ClearRecentConnectionsAction.ICON);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public set useConfirmationMessage(value: boolean) {
|
||||
this._useConfirmationMessage = value;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
if (this._useConfirmationMessage) {
|
||||
return this.promptConfirmationMessage().then(result => {
|
||||
if (result.confirmed) {
|
||||
this._connectionManagementService.clearRecentConnectionsList();
|
||||
this._onRecentConnectionsRemoved.fire();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return this.promptQuickOpenService().then(result => {
|
||||
if (result) {
|
||||
this._connectionManagementService.clearRecentConnectionsList();
|
||||
|
||||
const actions: INotificationActions = { primary: [] };
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: nls.localize('ClearedRecentConnections', "Recent connections list cleared"),
|
||||
actions
|
||||
});
|
||||
this._onRecentConnectionsRemoved.fire();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private promptQuickOpenService(): Promise<boolean> {
|
||||
const self = this;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let choices: { key, value }[] = [
|
||||
{ key: nls.localize('connectionAction.yes', "Yes"), value: true },
|
||||
{ key: nls.localize('connectionAction.no', "No"), value: false }
|
||||
];
|
||||
|
||||
self._quickInputService.pick(choices.map(x => x.key), { placeHolder: nls.localize('ClearRecentlyUsedLabel', "Clear List"), ignoreFocusLost: true }).then((choice) => {
|
||||
let confirm = find(choices, x => x.key === choice);
|
||||
resolve(confirm && confirm.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private promptConfirmationMessage(): Promise<IConfirmationResult> {
|
||||
let confirm: IConfirmation = {
|
||||
message: nls.localize('clearRecentConnectionMessage', "Are you sure you want to delete all the connections from the list?"),
|
||||
primaryButton: nls.localize('connectionDialog.yes', "Yes"),
|
||||
secondaryButton: nls.localize('connectionDialog.no', "No"),
|
||||
type: 'question'
|
||||
};
|
||||
|
||||
return new Promise<IConfirmationResult>((resolve, reject) => {
|
||||
this._dialogService.confirm(confirm).then((confirmed) => {
|
||||
resolve(confirmed);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to delete one recently used connection from the MRU
|
||||
*/
|
||||
export class ClearSingleRecentConnectionAction extends Action {
|
||||
|
||||
public static ID = 'clearSingleRecentConnectionAction';
|
||||
public static LABEL = nls.localize('delete', "Delete");
|
||||
private _onRecentConnectionRemoved = new Emitter<void>();
|
||||
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private _connectionProfile: IConnectionProfile,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
resolve(this._connectionManagementService.clearRecentConnection(this._connectionProfile));
|
||||
this._onRecentConnectionRemoved.fire();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to retrieve the current connection string
|
||||
*/
|
||||
export class GetCurrentConnectionStringAction extends Action {
|
||||
|
||||
public static ID = 'getCurrentConnectionStringAction';
|
||||
public static LABEL = nls.localize('connectionAction.GetCurrentConnectionString', "Get Current Connection String");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IEditorService private _editorService: IEditorService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
) {
|
||||
super(GetCurrentConnectionStringAction.ID, GetCurrentConnectionStringAction.LABEL);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let activeInput = this._editorService.activeEditor;
|
||||
if (activeInput && (activeInput instanceof QueryInput || activeInput instanceof EditDataInput || activeInput instanceof DashboardInput)
|
||||
&& this._connectionManagementService.isConnected(activeInput.uri)) {
|
||||
let includePassword = false;
|
||||
let connectionProfile = this._connectionManagementService.getConnectionProfile(activeInput.uri);
|
||||
this._connectionManagementService.getConnectionString(connectionProfile.id, includePassword).then(result => {
|
||||
|
||||
//Copy to clipboard
|
||||
this._clipboardService.writeText(result);
|
||||
|
||||
let message = result
|
||||
? result
|
||||
: nls.localize('connectionAction.connectionString', "Connection string not available");
|
||||
this._notificationService.info(message);
|
||||
});
|
||||
} else {
|
||||
let message = nls.localize('connectionAction.noConnection', "No active connection available");
|
||||
this._notificationService.info(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService';
|
||||
import * as TaskUtilities from 'sql/workbench/browser/taskUtilities';
|
||||
import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
// Connection status bar showing the current global connection
|
||||
export class ConnectionStatusbarItem extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
private static readonly ID = 'status.connection.status';
|
||||
|
||||
private statusItem: IStatusbarEntryAccessor;
|
||||
|
||||
constructor(
|
||||
@IStatusbarService private readonly statusbarService: IStatusbarService,
|
||||
@IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IObjectExplorerService private readonly objectExplorerService: IObjectExplorerService,
|
||||
) {
|
||||
super();
|
||||
this.statusItem = this._register(
|
||||
this.statusbarService.addEntry({
|
||||
text: '',
|
||||
},
|
||||
ConnectionStatusbarItem.ID,
|
||||
localize('status.connection.status', "Connection Status"),
|
||||
StatusbarAlignment.RIGHT, 100)
|
||||
);
|
||||
|
||||
this.hide();
|
||||
|
||||
this._register(this.connectionManagementService.onConnect(() => this._updateStatus()));
|
||||
this._register(this.connectionManagementService.onConnectionChanged(() => this._updateStatus()));
|
||||
this._register(this.connectionManagementService.onDisconnect(() => this._updateStatus()));
|
||||
this._register(this.editorService.onDidActiveEditorChange(() => this._updateStatus()));
|
||||
this._register(this.objectExplorerService.onSelectionOrFocusChange(() => this._updateStatus()));
|
||||
}
|
||||
|
||||
private hide() {
|
||||
this.statusbarService.updateEntryVisibility(ConnectionStatusbarItem.ID, false);
|
||||
}
|
||||
|
||||
private show() {
|
||||
this.statusbarService.updateEntryVisibility(ConnectionStatusbarItem.ID, true);
|
||||
}
|
||||
|
||||
// Update the connection status shown in the bar
|
||||
private _updateStatus(): void {
|
||||
let activeConnection = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService);
|
||||
if (activeConnection) {
|
||||
this._setConnectionText(activeConnection);
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Set connection info to connection status bar
|
||||
private _setConnectionText(connectionProfile: IConnectionProfile): void {
|
||||
let text: string = connectionProfile.serverName;
|
||||
if (text) {
|
||||
if (connectionProfile.databaseName && connectionProfile.databaseName !== '') {
|
||||
text = text + ' : ' + connectionProfile.databaseName;
|
||||
} else {
|
||||
text = text + ' : ' + '<default>';
|
||||
}
|
||||
}
|
||||
|
||||
let tooltip: string =
|
||||
'Server: ' + connectionProfile.serverName + '\r\n' +
|
||||
'Database: ' + (connectionProfile.databaseName ? connectionProfile.databaseName : '<default>') + '\r\n';
|
||||
|
||||
if (connectionProfile.userName && connectionProfile.userName !== '') {
|
||||
tooltip = tooltip + 'Login: ' + connectionProfile.userName + '\r\n';
|
||||
}
|
||||
|
||||
this.statusItem.update({
|
||||
text, tooltip
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>
|
||||
|
After Width: | Height: | Size: 139 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}.cls-2{fill:#3bb44a;}</style></defs><title>connected_active_server_16x16</title><path class="cls-1" d="M1.29.07V16h9.59a3.31,3.31,0,0,1-1.94-1H2.29V11h6a3.31,3.31,0,0,1,.54-.8A1.81,1.81,0,0,1,9,10H2.29v-9h8.53v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.3,1.8V4.25H5.75V1.8Zm2,2H3.8V2.3H5.25Z"/><circle class="cls-2" cx="11.24" cy="12.52" r="3.47"/></svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#3bb44a;}</style></defs><title>connected_active_server_inverse_16x16</title><path class="cls-1" d="M1.29.07V16h9.59a3.31,3.31,0,0,1-1.94-1H2.29V11h6a3.31,3.31,0,0,1,.54-.8A1.81,1.81,0,0,1,9,10H2.29v-9h8.53v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.3,1.8V4.25H5.75V1.8Zm2,2H3.8V2.3H5.25Z"/><circle class="cls-2" cx="11.24" cy="12.52" r="3.47"/></svg>
|
||||
|
After Width: | Height: | Size: 507 B |
@@ -0,0 +1,138 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* --- Registered servers tree viewlet --- */
|
||||
.server-explorer-viewlet .monaco-tree .monaco-tree-row .content .server-group {
|
||||
cursor: default;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Bold font style does not go well with CJK fonts */
|
||||
.server-explorer-viewlet:lang(zh-Hans) .monaco-tree .monaco-tree-row .server-group,
|
||||
.server-explorer-viewlet:lang(zh-Hant) .monaco-tree .monaco-tree-row .server-group,
|
||||
.server-explorer-viewlet:lang(ja) .monaco-tree .monaco-tree-row .server-group,
|
||||
.server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group { font-weight: normal; }
|
||||
|
||||
/* High Contrast Theming */
|
||||
.hc-black .monaco-workbench .server-explorer-viewlet .server-group {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.monaco-workbench > .activitybar .monaco-action-bar .action-label.serverTree {
|
||||
background-size: 22px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% !important;
|
||||
}
|
||||
|
||||
.server-explorer-viewlet .object-explorer-view {
|
||||
height: calc(100% - 36px);
|
||||
}
|
||||
|
||||
.server-explorer-viewlet .server-group {
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.server-explorer-viewlet .monaco-action-bar .action-label {
|
||||
margin-right: 0.3em;
|
||||
margin-left: 0.3em;
|
||||
line-height: 15px;
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
}
|
||||
|
||||
/* Add space beneath the button */
|
||||
.new-connection .monaco-text-button {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* display action buttons on hover */
|
||||
.server-explorer-viewlet .monaco-tree .monaco-tree-row > .content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Added to display the tree in connection dialog */
|
||||
.server-explorer-viewlet {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.explorer-servers {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* search box */
|
||||
.server-explorer-viewlet .search-box {
|
||||
padding-bottom: 4px;
|
||||
margin: auto;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
/* OE and connection element group */
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile,
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group {
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* OE and connection label */
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .label,
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .label {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* OE and connection icon */
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon,
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .object-element-group > .icon {
|
||||
float: left;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected {
|
||||
background: url('connected_active_server.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected,
|
||||
.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected{
|
||||
background: url('connected_active_server_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected {
|
||||
background: url('disconnected_server.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected,
|
||||
.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected{
|
||||
background: url('disconnected_server_inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
/* loading for OE node */
|
||||
.server-explorer-viewlet .monaco-tree .monaco-tree-rows > .monaco-tree-row > .codicon.in-progress .connection-tile:before,
|
||||
.server-explorer-viewlet .monaco-tree .monaco-tree-rows > .monaco-tree-row > .codicon.in-progress .object-element-group:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: -35px;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.expanded.has-children > .content.server-group:before {
|
||||
background: url('expanded-dark.svg') 50% 50% no-repeat;
|
||||
}
|
||||
|
||||
.monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content.server-group:before {
|
||||
background: url('collapsed-dark.svg') 50% 50% no-repeat;
|
||||
}
|
||||
|
||||
/* Add connection button */
|
||||
.server-explorer-viewlet .button-section {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}.cls-2{fill:#d02e00;}</style></defs><title>disconnected_server_16x16</title><path class="cls-1" d="M1.42.06V16H11a3.31,3.31,0,0,1-1.94-1H2.42V11h6a3.31,3.31,0,0,1,.54-.8,1.81,1.81,0,0,1,.19-.2H2.42v-9H11v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.43,1.79V4.24H5.89V1.79Zm2,2H3.93V2.29H5.39Z"/><path class="cls-2" d="M11.08,16a2.22,2.22,0,0,0,.45,0,2.59,2.59,0,0,0,.4,0Z"/><path class="cls-2" d="M12,9.08h0l-.39,0a3.68,3.68,0,0,0-.58,0A3.41,3.41,0,0,0,9.14,10a1.81,1.81,0,0,0-.19.2,3.46,3.46,0,0,0-.89,2.3,3.4,3.4,0,0,0,.85,2.26,1.29,1.29,0,0,0,.16.17A3.31,3.31,0,0,0,11,16H12a3.46,3.46,0,0,0,2.17-1.13,3.41,3.41,0,0,0,.88-2.3A3.47,3.47,0,0,0,12,9.08Zm0,5.72a1.72,1.72,0,0,1-.39,0,2.23,2.23,0,0,1-.77-.14,2.29,2.29,0,0,1-1.54-2.17A2.22,2.22,0,0,1,9.79,11a2.29,2.29,0,0,1,1-.67,2.23,2.23,0,0,1,.77-.14,1.72,1.72,0,0,1,.39,0h0a2.3,2.3,0,0,1,0,4.53Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1002 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#d02e00;}</style></defs><title>disconnected_server_inverse_16x16</title><path class="cls-1" d="M1.42.06V16H11a3.31,3.31,0,0,1-1.94-1H2.42V11h6a3.31,3.31,0,0,1,.54-.8,1.81,1.81,0,0,1,.19-.2H2.42v-9H11v8a3.68,3.68,0,0,1,.58,0l.39,0h0v-9Z"/><path class="cls-1" d="M3.43,1.79V4.24H5.89V1.79Zm2,2H3.93V2.29H5.39Z"/><path class="cls-2" d="M11.08,16a2.22,2.22,0,0,0,.45,0,2.59,2.59,0,0,0,.4,0Z"/><path class="cls-2" d="M12,9.08h0l-.39,0a3.68,3.68,0,0,0-.58,0A3.41,3.41,0,0,0,9.14,10a1.81,1.81,0,0,0-.19.2,3.46,3.46,0,0,0-.89,2.3,3.4,3.4,0,0,0,.85,2.26,1.29,1.29,0,0,0,.16.17A3.31,3.31,0,0,0,11,16H12a3.46,3.46,0,0,0,2.17-1.13,3.41,3.41,0,0,0,.88-2.3A3.47,3.47,0,0,0,12,9.08Zm0,5.72a1.72,1.72,0,0,1-.39,0,2.23,2.23,0,0,1-.77-.14,2.29,2.29,0,0,1-1.54-2.17A2.22,2.22,0,0,1,9.79,11a2.29,2.29,0,0,1,1-.67,2.23,2.23,0,0,1,.77-.14,1.72,1.72,0,0,1,.39,0h0a2.3,2.3,0,0,1,0,4.53Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1007 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M11 10H5.344L11 4.414V10z"/></svg>
|
||||
|
After Width: | Height: | Size: 118 B |
@@ -0,0 +1,136 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { ClearSingleRecentConnectionAction } from 'sql/workbench/contrib/connection/browser/connectionActions';
|
||||
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import mouse = require('vs/base/browser/mouseEvent');
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
|
||||
export class RecentConnectionActionsProvider extends ContributableActionProvider {
|
||||
private _onRecentConnectionRemoved = new Emitter<void>();
|
||||
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getRecentConnectionActions(tree: ITree, element: any): IAction[] {
|
||||
let actions: IAction[] = [];
|
||||
let clearSingleConnectionAction = this._instantiationService.createInstance(ClearSingleRecentConnectionAction, ClearSingleRecentConnectionAction.ID,
|
||||
ClearSingleRecentConnectionAction.LABEL, <IConnectionProfile>element);
|
||||
clearSingleConnectionAction.onRecentConnectionRemoved(() => this._onRecentConnectionRemoved.fire());
|
||||
actions.push(clearSingleConnectionAction);
|
||||
return actions;
|
||||
}
|
||||
|
||||
public hasActions(tree: ITree, element: any): boolean {
|
||||
return element instanceof ConnectionProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return actions given an element in the tree
|
||||
*/
|
||||
public getActions(tree: ITree, element: any): IAction[] {
|
||||
if (element instanceof ConnectionProfile) {
|
||||
return this.getRecentConnectionActions(tree, element);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class RecentConnectionsActionsContext {
|
||||
public connectionProfile: ConnectionProfile;
|
||||
public container: HTMLElement;
|
||||
public tree: ITree;
|
||||
}
|
||||
|
||||
export class RecentConnectionTreeController extends DefaultController {
|
||||
|
||||
private _onRecentConnectionRemoved = new Emitter<void>();
|
||||
public onRecentConnectionRemoved: Event<void> = this._onRecentConnectionRemoved.event;
|
||||
|
||||
constructor(
|
||||
private clickcb: (element: any, eventish: ICancelableEvent, origin: string) => void,
|
||||
private actionProvider: RecentConnectionActionsProvider,
|
||||
private _connectionManagementService: IConnectionManagementService,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
|
||||
this.clickcb(element, eventish, origin);
|
||||
return super.onLeftClick(tree, element, eventish, origin);
|
||||
}
|
||||
|
||||
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
|
||||
super.onEnter(tree, event);
|
||||
this.clickcb(tree.getSelection()[0], event, 'keyboard');
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onRightClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
|
||||
this.clickcb(element, eventish, origin);
|
||||
this.showContextMenu(tree, element, eventish);
|
||||
return true;
|
||||
}
|
||||
|
||||
public onMouseDown(tree: ITree, element: any, event: mouse.IMouseEvent, origin: string = 'mouse'): boolean {
|
||||
if (event.leftButton || event.middleButton) {
|
||||
return this.onLeftClick(tree, element, event, origin);
|
||||
} else {
|
||||
return this.onRightClick(tree, element, event);
|
||||
}
|
||||
}
|
||||
|
||||
public onKeyDown(tree: ITree, event: IKeyboardEvent): boolean {
|
||||
if (event.keyCode === 20) {
|
||||
let element = tree.getFocus();
|
||||
if (element instanceof ConnectionProfile) {
|
||||
this._connectionManagementService.clearRecentConnection(element);
|
||||
this._onRecentConnectionRemoved.fire();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onKeyDown(tree, event);
|
||||
}
|
||||
|
||||
public showContextMenu(tree: ITree, element: any, event: any): boolean {
|
||||
let actionContext: any;
|
||||
|
||||
if (element instanceof ConnectionProfile) {
|
||||
actionContext = new RecentConnectionsActionsContext();
|
||||
actionContext.container = event.target;
|
||||
actionContext.connectionProfile = <ConnectionProfile>element;
|
||||
actionContext.tree = tree;
|
||||
} else {
|
||||
actionContext = element;
|
||||
}
|
||||
|
||||
let anchor = { x: event.x + 1, y: event.y };
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => this.actionProvider.getActions(tree, element),
|
||||
onHide: (wasCancelled?: boolean) => {
|
||||
if (wasCancelled) {
|
||||
tree.domFocus();
|
||||
}
|
||||
},
|
||||
getActionsContext: () => (actionContext)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
|
||||
export class SavedConnectionTreeController extends DefaultController {
|
||||
constructor(private clickcb: (element: any, eventish: ICancelableEvent, origin: string) => void) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
|
||||
this.clickcb(element, eventish, origin);
|
||||
return super.onLeftClick(tree, element, eventish, origin);
|
||||
}
|
||||
|
||||
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
|
||||
super.onEnter(tree, event);
|
||||
|
||||
// grab the current selection for use later
|
||||
let selection = tree.getSelection();
|
||||
|
||||
this.clickcb(selection[0], event, 'keyboard');
|
||||
tree.toggleExpansion(selection[0]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
27
src/sql/workbench/contrib/connection/common/connection.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// ------------------------------- < Cancel Connect Request > ---------------------------------------
|
||||
|
||||
/**
|
||||
* Cancel connect request message format
|
||||
*/
|
||||
export class CancelConnectParams {
|
||||
/**
|
||||
* URI identifying the owner of the connection
|
||||
*/
|
||||
public ownerUri: string;
|
||||
}
|
||||
|
||||
// ------------------------------- </ Cancel Connect Request > --------------------------------------
|
||||
|
||||
// ------------------------------- < Disconnect Request > -------------------------------------------
|
||||
|
||||
// Disconnect request message format
|
||||
export class DisconnectParams {
|
||||
// URI identifying the owner of the connection
|
||||
public ownerUri: string;
|
||||
}
|
||||
|
||||
// ------------------------------- </ Disconnect Request > ------------------------------------------
|
||||
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IConnectionProfile } from 'azdata';
|
||||
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
|
||||
|
||||
export class ConnectionContextKey implements IContextKey<IConnectionProfile> {
|
||||
|
||||
static Provider = new RawContextKey<string>('connectionProvider', undefined);
|
||||
static Server = new RawContextKey<string>('serverName', undefined);
|
||||
static Database = new RawContextKey<string>('databaseName', undefined);
|
||||
static Connection = new RawContextKey<IConnectionProfile>('connection', undefined);
|
||||
static IsQueryProvider = new RawContextKey<boolean>('isQueryProvider', false);
|
||||
|
||||
private _providerKey: IContextKey<string>;
|
||||
private _serverKey: IContextKey<string>;
|
||||
private _databaseKey: IContextKey<string>;
|
||||
private _connectionKey: IContextKey<IConnectionProfile>;
|
||||
private _isQueryProviderKey: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IQueryManagementService private queryManagementService: IQueryManagementService
|
||||
) {
|
||||
this._providerKey = ConnectionContextKey.Provider.bindTo(contextKeyService);
|
||||
this._serverKey = ConnectionContextKey.Server.bindTo(contextKeyService);
|
||||
this._databaseKey = ConnectionContextKey.Database.bindTo(contextKeyService);
|
||||
this._connectionKey = ConnectionContextKey.Connection.bindTo(contextKeyService);
|
||||
this._isQueryProviderKey = ConnectionContextKey.IsQueryProvider.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
set(value: IConnectionProfile) {
|
||||
let queryProviders = this.queryManagementService.getRegisteredProviders();
|
||||
this._connectionKey.set(value);
|
||||
this._providerKey.set(value && value.providerName);
|
||||
this._serverKey.set(value && value.serverName);
|
||||
this._databaseKey.set(value && value.databaseName);
|
||||
this._isQueryProviderKey.set(value && value.providerName && queryProviders.indexOf(value.providerName) !== -1);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._providerKey.reset();
|
||||
this._serverKey.reset();
|
||||
this._databaseKey.reset();
|
||||
this._connectionKey.reset();
|
||||
this._isQueryProviderKey.reset();
|
||||
}
|
||||
|
||||
public get(): IConnectionProfile {
|
||||
return this._connectionKey.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { ConnectionSummary } from 'azdata';
|
||||
import * as LocalizedConstants from 'sql/workbench/contrib/connection/common/localizedConstants';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
// Status when making connections from the viewlet
|
||||
export class ConnectionGlobalStatus {
|
||||
|
||||
private _displayTime: number = 5000; // (in ms)
|
||||
|
||||
constructor(
|
||||
@INotificationService private _notificationService: INotificationService
|
||||
) {
|
||||
}
|
||||
|
||||
public setStatusToConnected(connectionSummary: ConnectionSummary): void {
|
||||
if (this._notificationService) {
|
||||
let text: string;
|
||||
let connInfo: string = connectionSummary.serverName;
|
||||
if (connInfo) {
|
||||
if (connectionSummary.databaseName && connectionSummary.databaseName !== '') {
|
||||
connInfo = connInfo + ' : ' + connectionSummary.databaseName;
|
||||
} else {
|
||||
connInfo = connInfo + ' : ' + '<default>';
|
||||
}
|
||||
text = LocalizedConstants.onDidConnectMessage + ' ' + connInfo;
|
||||
}
|
||||
this._notificationService.status(text, { hideAfter: this._displayTime });
|
||||
}
|
||||
}
|
||||
|
||||
public setStatusToDisconnected(fileUri: string): void {
|
||||
if (this._notificationService) {
|
||||
this._notificationService.status(LocalizedConstants.onDidDisconnectMessage, { hideAfter: this._displayTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
|
||||
export const Extensions = {
|
||||
ConnectionProviderContributions: 'connection.providers'
|
||||
};
|
||||
|
||||
export interface IConnectionProviderRegistry {
|
||||
registerConnectionProvider(id: string, properties: ConnectionProviderProperties): void;
|
||||
getProperties(id: string): ConnectionProviderProperties | undefined;
|
||||
readonly onNewProvider: Event<{ id: string, properties: ConnectionProviderProperties }>;
|
||||
readonly providers: { [id: string]: ConnectionProviderProperties };
|
||||
}
|
||||
|
||||
class ConnectionProviderRegistryImpl implements IConnectionProviderRegistry {
|
||||
private _providers = new Map<string, ConnectionProviderProperties>();
|
||||
private _onNewProvider = new Emitter<{ id: string, properties: ConnectionProviderProperties }>();
|
||||
public readonly onNewProvider: Event<{ id: string, properties: ConnectionProviderProperties }> = this._onNewProvider.event;
|
||||
|
||||
public registerConnectionProvider(id: string, properties: ConnectionProviderProperties): void {
|
||||
this._providers.set(id, properties);
|
||||
this._onNewProvider.fire({ id, properties });
|
||||
}
|
||||
|
||||
public getProperties(id: string): ConnectionProviderProperties | undefined {
|
||||
return this._providers.get(id);
|
||||
}
|
||||
|
||||
public get providers(): { [id: string]: ConnectionProviderProperties } {
|
||||
let rt: { [id: string]: ConnectionProviderProperties } = {};
|
||||
this._providers.forEach((v, k) => {
|
||||
rt[k] = deepClone(v);
|
||||
});
|
||||
return rt;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionRegistry = new ConnectionProviderRegistryImpl();
|
||||
Registry.add(Extensions.ConnectionProviderContributions, connectionRegistry);
|
||||
|
||||
const ConnectionProviderContrib: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
providerId: {
|
||||
type: 'string',
|
||||
description: localize('schema.providerId', "Common id for the provider")
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
description: localize('schema.displayName', "Display Name for the provider")
|
||||
},
|
||||
iconPath: {
|
||||
description: localize('schema.iconPath', "Icon path for the server type"),
|
||||
oneOf: [
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
path: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
type: 'string',
|
||||
},
|
||||
dark: {
|
||||
type: 'string',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
type: 'string',
|
||||
},
|
||||
dark: {
|
||||
type: 'string',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'string'
|
||||
}
|
||||
]
|
||||
},
|
||||
connectionOptions: {
|
||||
type: 'array',
|
||||
description: localize('schema.connectionOptions', "Options for connection"),
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
specialValueType: {
|
||||
type: 'string'
|
||||
},
|
||||
isIdentity: {
|
||||
type: 'boolean'
|
||||
},
|
||||
name: {
|
||||
type: 'string'
|
||||
},
|
||||
displayName: {
|
||||
type: 'string'
|
||||
},
|
||||
description: {
|
||||
type: 'string'
|
||||
},
|
||||
groupName: {
|
||||
type: 'string'
|
||||
},
|
||||
valueType: {
|
||||
type: 'string'
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'any'
|
||||
},
|
||||
objectType: {
|
||||
type: 'any'
|
||||
},
|
||||
categoryValues: {
|
||||
type: 'any'
|
||||
},
|
||||
isRequired: {
|
||||
type: 'boolean'
|
||||
},
|
||||
isArray: {
|
||||
type: 'bolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['providerId']
|
||||
};
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<ConnectionProviderProperties | ConnectionProviderProperties[]>({ extensionPoint: 'connectionProvider', jsonSchema: ConnectionProviderContrib }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(contrib: ConnectionProviderProperties, extension: IExtensionPointUser<any>) {
|
||||
connectionRegistry.registerConnectionProvider(contrib.providerId, contrib);
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
resolveIconPath(extension);
|
||||
if (Array.isArray<ConnectionProviderProperties>(value)) {
|
||||
for (let command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function resolveIconPath(extension: IExtensionPointUser<any>): void {
|
||||
if (!extension || !extension.value) { return undefined; }
|
||||
|
||||
let toAbsolutePath = (iconPath: any) => {
|
||||
if (!iconPath || !baseDir) { return; }
|
||||
if (Array.isArray(iconPath)) {
|
||||
for (let e of iconPath) {
|
||||
e.path = {
|
||||
light: resources.joinPath(extension.description.extensionLocation, e.path.light.toString()),
|
||||
dark: resources.joinPath(extension.description.extensionLocation, e.path.dark.toString())
|
||||
};
|
||||
}
|
||||
} else if (typeof iconPath === 'string') {
|
||||
iconPath = {
|
||||
light: resources.joinPath(extension.description.extensionLocation, iconPath),
|
||||
dark: resources.joinPath(extension.description.extensionLocation, iconPath)
|
||||
};
|
||||
} else {
|
||||
iconPath = {
|
||||
light: resources.joinPath(extension.description.extensionLocation, iconPath.light.toString()),
|
||||
dark: resources.joinPath(extension.description.extensionLocation, iconPath.dark.toString())
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let baseDir = extension.description.extensionLocation.fsPath;
|
||||
let properties: ConnectionProviderProperties = extension.value;
|
||||
if (Array.isArray<ConnectionProviderProperties>(properties)) {
|
||||
for (let p of properties) {
|
||||
toAbsolutePath(p['iconPath']);
|
||||
}
|
||||
} else {
|
||||
toAbsolutePath(properties['iconPath']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export const onDidConnectMessage = localize('onDidConnectMessage', "Connected to");
|
||||
export const onDidDisconnectMessage = localize('onDidDisconnectMessage', "Disconnected");
|
||||
export const unsavedGroupLabel = localize('unsavedGroupLabel', "Unsaved Connections");
|
||||
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServerInfo } from 'azdata';
|
||||
import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
|
||||
export class ServerInfoContextKey implements IContextKey<ServerInfo> {
|
||||
|
||||
static ServerInfo = new RawContextKey<ServerInfo>('serverInfo', undefined);
|
||||
static ServerMajorVersion = new RawContextKey<string>('serverMajorVersion', undefined);
|
||||
static IsCloud = new RawContextKey<boolean>('isCloud', undefined);
|
||||
static IsBigDataCluster = new RawContextKey<boolean>('isBigDataCluster', undefined);
|
||||
static EngineEdition = new RawContextKey<number>('engineEdition', undefined);
|
||||
|
||||
private _serverInfo: IContextKey<ServerInfo>;
|
||||
private _serverMajorVersion: IContextKey<string>;
|
||||
private _isCloud: IContextKey<boolean>;
|
||||
private _isBigDataCluster: IContextKey<boolean>;
|
||||
private _engineEdition: IContextKey<number>;
|
||||
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
this._serverInfo = ServerInfoContextKey.ServerInfo.bindTo(contextKeyService);
|
||||
this._serverMajorVersion = ServerInfoContextKey.ServerMajorVersion.bindTo(contextKeyService);
|
||||
this._isCloud = ServerInfoContextKey.IsCloud.bindTo(contextKeyService);
|
||||
this._isBigDataCluster = ServerInfoContextKey.IsBigDataCluster.bindTo(contextKeyService);
|
||||
this._engineEdition = ServerInfoContextKey.EngineEdition.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
set(value: ServerInfo) {
|
||||
this._serverInfo.set(value);
|
||||
let majorVersion = value && value.serverMajorVersion;
|
||||
this._serverMajorVersion.set(majorVersion && `${majorVersion}`);
|
||||
this._isCloud.set(value && value.isCloud);
|
||||
this._isBigDataCluster.set(value && value.options && value.options['isBigDataCluster']);
|
||||
let engineEditionId = value && value.engineEditionId;
|
||||
engineEditionId ? this._engineEdition.set(engineEditionId) : this._engineEdition.set(DatabaseEngineEdition.Unknown);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._serverMajorVersion.reset();
|
||||
this._isCloud.reset();
|
||||
this._isBigDataCluster.reset();
|
||||
this._engineEdition.reset();
|
||||
}
|
||||
|
||||
public get(): ServerInfo {
|
||||
return this._serverInfo.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OptionsDialog } from 'sql/workbench/browser/modal/optionsDialog';
|
||||
import { AdvancedPropertiesController } from 'sql/workbench/contrib/connection/browser/advancedPropertiesController';
|
||||
import * as azdata from 'azdata';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as assert from 'assert';
|
||||
import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
|
||||
suite('Advanced properties dialog tests', () => {
|
||||
let advancedController: AdvancedPropertiesController;
|
||||
let providerOptions: azdata.ConnectionOption[];
|
||||
|
||||
setup(() => {
|
||||
advancedController = new AdvancedPropertiesController(() => { }, null);
|
||||
providerOptions = [
|
||||
{
|
||||
name: 'a1',
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
groupName: 'a',
|
||||
categoryValues: undefined,
|
||||
defaultValue: undefined,
|
||||
isIdentity: true,
|
||||
isRequired: true,
|
||||
specialValueType: null,
|
||||
valueType: ServiceOptionType.string
|
||||
},
|
||||
{
|
||||
name: 'b1',
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
groupName: 'b',
|
||||
categoryValues: undefined,
|
||||
defaultValue: undefined,
|
||||
isIdentity: true,
|
||||
isRequired: true,
|
||||
specialValueType: null,
|
||||
valueType: ServiceOptionType.string
|
||||
},
|
||||
{
|
||||
name: 'noType',
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
groupName: undefined,
|
||||
categoryValues: undefined,
|
||||
defaultValue: undefined,
|
||||
isIdentity: true,
|
||||
isRequired: true,
|
||||
specialValueType: null,
|
||||
valueType: ServiceOptionType.string
|
||||
},
|
||||
{
|
||||
name: 'a2',
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
groupName: 'a',
|
||||
categoryValues: undefined,
|
||||
defaultValue: undefined,
|
||||
isIdentity: true,
|
||||
isRequired: true,
|
||||
specialValueType: null,
|
||||
valueType: ServiceOptionType.string
|
||||
},
|
||||
{
|
||||
name: 'b2',
|
||||
displayName: undefined,
|
||||
description: undefined,
|
||||
groupName: 'b',
|
||||
categoryValues: undefined,
|
||||
defaultValue: undefined,
|
||||
isIdentity: true,
|
||||
isRequired: true,
|
||||
specialValueType: null,
|
||||
valueType: ServiceOptionType.string
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
test('advanced dialog should open when showDialog in advancedController get called', () => {
|
||||
let isAdvancedDialogCalled = false;
|
||||
let options: { [name: string]: any } = {};
|
||||
let advanceDialog = TypeMoq.Mock.ofType(OptionsDialog, TypeMoq.MockBehavior.Strict,
|
||||
'', // title
|
||||
'', // name
|
||||
{}, // options
|
||||
undefined, // partsService
|
||||
undefined, // themeService
|
||||
undefined, // Context view service
|
||||
undefined, // instantiation Service
|
||||
undefined, // telemetry service
|
||||
new MockContextKeyService() // contextkeyservice
|
||||
);
|
||||
advanceDialog.setup(x => x.open(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback(() => {
|
||||
isAdvancedDialogCalled = true;
|
||||
});
|
||||
advancedController.advancedDialog = advanceDialog.object;
|
||||
advancedController.showDialog(providerOptions, options);
|
||||
assert.equal(isAdvancedDialogCalled, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { registerContainer, generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardNavSection.contribution';
|
||||
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.contribution';
|
||||
import { WEBVIEW_CONTAINER } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWebviewContainer.contribution';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
import { NavSectionConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
|
||||
const containerTypes = [
|
||||
WIDGETS_CONTAINER,
|
||||
GRID_CONTAINER,
|
||||
WEBVIEW_CONTAINER,
|
||||
NAV_SECTION
|
||||
];
|
||||
|
||||
export type IUserFriendlyIcon = string | { light: string; dark: string; };
|
||||
|
||||
export interface IDashboardContainerContrib {
|
||||
id: string;
|
||||
container: Record<string, NavSectionConfig[]>;
|
||||
}
|
||||
|
||||
const containerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: localize('azdata.extension.contributes.dashboard.container.id', "Unique identifier for this container.")
|
||||
},
|
||||
container: {
|
||||
description: localize('azdata.extension.contributes.dashboard.container.container', "The container that will be displayed in the tab."),
|
||||
type: 'object',
|
||||
properties: generateContainerTypeSchemaProperties()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const containerContributionSchema: IJSONSchema = {
|
||||
description: localize('azdata.extension.contributes.containers', "Contributes a single or multiple dashboard containers for users to add to their dashboard."),
|
||||
oneOf: [
|
||||
containerSchema,
|
||||
{
|
||||
type: 'array',
|
||||
items: containerSchema
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<IDashboardContainerContrib | IDashboardContainerContrib[]>({ extensionPoint: 'dashboard.containers', jsonSchema: containerContributionSchema }).setHandler(extensions => {
|
||||
|
||||
function handleCommand(dashboardContainer: IDashboardContainerContrib, extension: IExtensionPointUser<any>) {
|
||||
const { id, container } = dashboardContainer;
|
||||
if (!id) {
|
||||
extension.collector.error(localize('dashboardContainer.contribution.noIdError', "No id in dashboard container specified for extension."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
extension.collector.error(localize('dashboardContainer.contribution.noContainerError', "No container in dashboard container specified for extension."));
|
||||
return;
|
||||
}
|
||||
if (Object.keys(container).length !== 1) {
|
||||
extension.collector.error(localize('dashboardContainer.contribution.moreThanOneDashboardContainersError', "Exactly 1 dashboard container must be defined per space."));
|
||||
return;
|
||||
}
|
||||
|
||||
let result = true;
|
||||
const containerkey = Object.keys(container)[0];
|
||||
const containerValue = values(container)[0];
|
||||
|
||||
const containerTypeFound = find(containerTypes, c => c === containerkey);
|
||||
if (!containerTypeFound) {
|
||||
extension.collector.error(localize('dashboardTab.contribution.unKnownContainerType', "Unknown container type defines in dashboard container for extension."));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (containerkey) {
|
||||
case WIDGETS_CONTAINER:
|
||||
result = validateWidgetContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case GRID_CONTAINER:
|
||||
result = validateGridContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case NAV_SECTION:
|
||||
result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
registerContainer({ id, container });
|
||||
}
|
||||
}
|
||||
|
||||
for (const extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<IDashboardContainerContrib>(value)) {
|
||||
for (const command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardControlHostContainer';
|
||||
|
||||
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { ControlHostContent } from 'sql/workbench/contrib/dashboard/browser/contents/controlHostContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-controlhost-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardControlHostContainer) }],
|
||||
template: `
|
||||
<controlhost-content [webviewId]="tab.id">
|
||||
</controlhost-content>
|
||||
`
|
||||
})
|
||||
|
||||
export class DashboardControlHostContainer extends DashboardTab implements AfterContentInit {
|
||||
@Input() private tab: TabConfig;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(ControlHostContent) private _hostContent: ControlHostContent;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._hostContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
|
||||
const container = <any>this.tab.container;
|
||||
if (container['controlhost-container'] && container['controlhost-container'].type) {
|
||||
this._hostContent.setControlType(container['controlhost-container'].type);
|
||||
}
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._hostContent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this._hostContent.refresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const CONTROLHOST_CONTAINER = 'controlhost-container';
|
||||
|
||||
let webviewSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('dashboard.container.controlhost', "The controlhost that will be displayed in this tab."),
|
||||
default: null
|
||||
};
|
||||
|
||||
registerContainerType(CONTROLHOST_CONTAINER, webviewSchema);
|
||||
registerNavSectionContainerType(CONTROLHOST_CONTAINER, webviewSchema);
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-controlhost-container {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardErrorContainer';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, ElementRef, ChangeDetectorRef, AfterViewInit } from '@angular/core';
|
||||
|
||||
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-error-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardErrorContainer) }],
|
||||
template: `
|
||||
<div class="error-container">
|
||||
<div class="codicon globalError">
|
||||
</div>
|
||||
<div class="error-message" #errorMessage>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class DashboardErrorContainer extends DashboardTab implements AfterViewInit {
|
||||
@Input() private tab: TabConfig;
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild('errorMessage', { read: ElementRef }) private _errorMessageContainer: ElementRef;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
const errorMessage = this._errorMessageContainer.nativeElement as HTMLElement;
|
||||
errorMessage.innerText = nls.localize('dashboardNavSection.loadTabError', "The \"{0}\" section has invalid content. Please contact extension owner.", this.tab.title);
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-error-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
dashboard-error-container .error-container {
|
||||
padding: 6px;
|
||||
background: #D02E00;
|
||||
color: white;
|
||||
}
|
||||
|
||||
dashboard-error-container .error-container .icon.globalError {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
float: left;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div class="fullsize" style="display: flex; flex-direction: column">
|
||||
<div scrollable [horizontalScroll]="ScrollbarVisibility.Auto" [verticalScroll]="ScrollbarVisibility.Auto">
|
||||
<table class="grid-table">
|
||||
<tr *ngFor="let row of rows" class="grid-table-row">
|
||||
<ng-container *ngFor="let col of cols">
|
||||
<ng-container *ngIf="getContent(row,col) !== undefined">
|
||||
<td class="table-cell" [colSpan]=getColspan(row,col) [rowSpan]=getRowspan(row,col)
|
||||
[width]="getWidgetWidth(row,col)" [height]="getWidgetHeight(row,col)">
|
||||
|
||||
<dashboard-widget-wrapper *ngIf="isWidget(row,col)" [_config]="getWidgetContent(row,col)"
|
||||
style="position:absolute;" [style.width]="getWidgetWidth(row,col)"
|
||||
[style.height]="getWidgetHeight(row,col)">
|
||||
</dashboard-widget-wrapper>
|
||||
|
||||
<webview-content *ngIf="isWebview(row,col)" [webviewId]="getWebviewId(row,col)">
|
||||
</webview-content>
|
||||
|
||||
<modelview-content *ngIf="isModelView(row,col)" [modelViewId]="getModelViewId(row,col)">
|
||||
</modelview-content>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,260 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardGridContainer';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ElementRef, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, ContentChild } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { TabConfig, WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { DashboardWidgetWrapper } from 'sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.component';
|
||||
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { WebviewContent } from 'sql/workbench/contrib/dashboard/browser/contents/webviewContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums';
|
||||
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { fill } from 'vs/base/common/arrays';
|
||||
|
||||
export interface GridCellConfig {
|
||||
id?: string;
|
||||
row?: number;
|
||||
col?: number;
|
||||
colspan?: string | number;
|
||||
rowspan?: string | number;
|
||||
}
|
||||
|
||||
export interface GridWidgetConfig extends GridCellConfig, WidgetConfig {
|
||||
}
|
||||
|
||||
export interface GridWebviewConfig extends GridCellConfig {
|
||||
webview: {
|
||||
id?: string;
|
||||
};
|
||||
}
|
||||
export interface GridModelViewConfig extends GridCellConfig {
|
||||
widget: {
|
||||
modelview: {
|
||||
id?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-grid-container',
|
||||
templateUrl: decodeURI(require.toUrl('./dashboardGridContainer.component.html')),
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardGridContainer) }]
|
||||
})
|
||||
export class DashboardGridContainer extends DashboardTab implements OnDestroy {
|
||||
@Input() private tab: TabConfig;
|
||||
private _contents: GridCellConfig[];
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
private cellWidth: number = 270;
|
||||
private cellHeight: number = 270;
|
||||
public ScrollbarVisibility = ScrollbarVisibility;
|
||||
|
||||
protected SKELETON_WIDTH = 5;
|
||||
|
||||
protected rows: number[];
|
||||
protected cols: number[];
|
||||
|
||||
protected getContent(row: number, col: number): GridCellConfig {
|
||||
const widget = this._contents.filter(w => w.row === row && w.col === col);
|
||||
return widget ? widget[0] : undefined;
|
||||
}
|
||||
|
||||
protected getWidgetContent(row: number, col: number): GridWidgetConfig {
|
||||
const content = this.getContent(row, col);
|
||||
if (content) {
|
||||
const widgetConfig = <GridWidgetConfig>content;
|
||||
if (widgetConfig && widgetConfig.widget) {
|
||||
return widgetConfig;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getWebviewContent(row: number, col: number): GridWebviewConfig {
|
||||
const content = this.getContent(row, col);
|
||||
if (content) {
|
||||
const webviewConfig = <GridWebviewConfig>content;
|
||||
if (webviewConfig && webviewConfig.webview) {
|
||||
return webviewConfig;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected getModelViewContent(row: number, col: number): GridModelViewConfig {
|
||||
const content = this.getContent(row, col);
|
||||
if (content) {
|
||||
const modelviewConfig = <GridModelViewConfig>content;
|
||||
if (modelviewConfig && modelviewConfig.widget.modelview) {
|
||||
return modelviewConfig;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
protected isWidget(row: number, col: number): boolean {
|
||||
const widgetConfig = this.getWidgetContent(row, col);
|
||||
return widgetConfig !== undefined;
|
||||
}
|
||||
|
||||
protected isWebview(row: number, col: number): boolean {
|
||||
const webview = this.getWebviewContent(row, col);
|
||||
return webview !== undefined;
|
||||
}
|
||||
|
||||
protected getWebviewId(row: number, col: number): string {
|
||||
const widgetConfig = this.getWebviewContent(row, col);
|
||||
if (widgetConfig && widgetConfig.webview) {
|
||||
return widgetConfig.webview.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected isModelView(row: number, col: number): boolean {
|
||||
const modelView = this.getModelViewContent(row, col);
|
||||
return modelView !== undefined;
|
||||
}
|
||||
|
||||
protected getModelViewId(row: number, col: number): string {
|
||||
const widgetConfig = this.getModelViewContent(row, col);
|
||||
if (widgetConfig && widgetConfig.widget.modelview) {
|
||||
return widgetConfig.widget.modelview.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
protected getColspan(row: number, col: number): string {
|
||||
const content = this.getContent(row, col);
|
||||
let colspan: string = '1';
|
||||
if (content && content.colspan) {
|
||||
colspan = this.convertToNumber(content.colspan, this.cols.length).toString();
|
||||
}
|
||||
return colspan;
|
||||
}
|
||||
|
||||
protected getRowspan(row: number, col: number): string {
|
||||
const content = this.getContent(row, col);
|
||||
if (content && (content.rowspan)) {
|
||||
return this.convertToNumber(content.rowspan, this.rows.length).toString();
|
||||
} else {
|
||||
return '1';
|
||||
}
|
||||
}
|
||||
|
||||
protected getWidgetWidth(row: number, col: number): string {
|
||||
const colspan = this.getColspan(row, col);
|
||||
const columnCount = this.convertToNumber(colspan, this.cols.length);
|
||||
|
||||
return columnCount * this.cellWidth + 'px';
|
||||
}
|
||||
|
||||
protected getWidgetHeight(row: number, col: number): string {
|
||||
const rowspan = this.getRowspan(row, col);
|
||||
const rowCount = this.convertToNumber(rowspan, this.rows.length);
|
||||
|
||||
return rowCount * this.cellHeight + 'px';
|
||||
}
|
||||
|
||||
private convertToNumber(value: string | number, maxNumber: number): number {
|
||||
if (!value) {
|
||||
return 1;
|
||||
}
|
||||
if (value === '*') {
|
||||
return maxNumber;
|
||||
}
|
||||
try {
|
||||
return +value;
|
||||
} catch {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ViewChildren(DashboardWidgetWrapper) private _widgets: QueryList<DashboardWidgetWrapper>;
|
||||
@ViewChildren(WebviewContent) private _webViews: QueryList<WebviewContent>;
|
||||
@ContentChild(ScrollableDirective) private _scrollable: ScrollableDirective;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ElementRef)) protected _el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.tab.container) {
|
||||
this._contents = values(this.tab.container)[0];
|
||||
this._contents.forEach(widget => {
|
||||
if (!widget.row) {
|
||||
widget.row = 0;
|
||||
}
|
||||
if (!widget.col) {
|
||||
widget.col = 0;
|
||||
}
|
||||
if (!widget.colspan) {
|
||||
widget.colspan = '1';
|
||||
}
|
||||
if (!widget.rowspan) {
|
||||
widget.rowspan = '1';
|
||||
}
|
||||
});
|
||||
this.rows = this.createIndexes(this._contents.map(w => w.row));
|
||||
this.cols = this.createIndexes(this._contents.map(w => w.col));
|
||||
}
|
||||
}
|
||||
|
||||
private createIndexes(indexes: number[]) {
|
||||
const max = Math.max(...indexes) + 1;
|
||||
return fill(max, 0).map((x, i) => i);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.layout();
|
||||
});
|
||||
}
|
||||
if (this._webViews) {
|
||||
this._webViews.forEach(item => {
|
||||
item.layout();
|
||||
});
|
||||
}
|
||||
if (this._scrollable) {
|
||||
this._scrollable.layout();
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._widgets) {
|
||||
this._widgets.forEach(item => {
|
||||
item.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { generateDashboardGridLayoutSchema } from 'sql/workbench/contrib/dashboard/browser/pages/dashboardPageContribution';
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
export const GRID_CONTAINER = 'grid-container';
|
||||
|
||||
let gridContainersSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.container.gridtab.items', "The list of widgets or webviews that will be displayed in this tab."),
|
||||
items: generateDashboardGridLayoutSchema(undefined, true)
|
||||
};
|
||||
|
||||
registerContainerType(GRID_CONTAINER, gridContainersSchema);
|
||||
registerNavSectionContainerType(GRID_CONTAINER, gridContainersSchema);
|
||||
|
||||
export function validateGridContainerContribution(extension: IExtensionPointUser<any>, gridConfigs: object[]): boolean {
|
||||
let result = true;
|
||||
gridConfigs.forEach(widgetConfig => {
|
||||
const allKeys = Object.keys(widgetConfig);
|
||||
const widgetOrWebviewKey = find(allKeys, key => key === 'widget' || key === 'webview');
|
||||
if (!widgetOrWebviewKey) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('gridContainer.invalidInputs', "widgets or webviews are expected inside widgets-container for extension."));
|
||||
return;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-tab {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.grid-table {
|
||||
border-spacing: 5px;
|
||||
}
|
||||
|
||||
.grid-table-row {
|
||||
width: auto;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.fullsize {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardHomeContainer';
|
||||
|
||||
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild, ContentChild } from '@angular/core';
|
||||
|
||||
import { DashboardWidgetContainer } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.component';
|
||||
import { WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { DashboardServiceInterface } from 'sql/workbench/contrib/dashboard/browser/services/dashboardServiceInterface.service';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { AngularEventType, IAngularEventingService } from 'sql/platform/angularEventing/browser/angularEventingService';
|
||||
import { DashboardWidgetWrapper } from 'sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.component';
|
||||
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-home-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardHomeContainer) }],
|
||||
template: `
|
||||
<div class="fullsize" style="display: flex; flex-direction: column">
|
||||
<div scrollable [horizontalScroll]="ScrollbarVisibility.Hidden" [verticalScroll]="ScrollbarVisibility.Auto">
|
||||
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
|
||||
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
|
||||
</dashboard-widget-wrapper>
|
||||
<widget-content style="flex: 1" [scrollContent]="false" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
|
||||
</widget-content>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class DashboardHomeContainer extends DashboardWidgetContainer {
|
||||
@Input() private properties: WidgetConfig;
|
||||
@ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper;
|
||||
@ContentChild(ScrollableDirective) private _scrollable: ScrollableDirective;
|
||||
|
||||
public ScrollbarVisibility = ScrollbarVisibility;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: DashboardServiceInterface,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
|
||||
@Inject(IAngularEventingService) private angularEventingService: IAngularEventingService
|
||||
) {
|
||||
super(_cd);
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
const collapsedVal = this.dashboardService.getSettings<string>(`${this.properties.context}.properties`);
|
||||
if (collapsedVal === 'collapsed') {
|
||||
this._propertiesClass.collapsed = true;
|
||||
}
|
||||
this.angularEventingService.onAngularEvent(this.dashboardService.getUnderlyingUri(), event => {
|
||||
if (event.event === AngularEventType.COLLAPSE_WIDGET && this._propertiesClass && event.payload === this._propertiesClass.guid) {
|
||||
this._propertiesClass.collapsed = !this._propertiesClass.collapsed;
|
||||
this._cd.detectChanges();
|
||||
this._configurationService.updateValue(`dashboard.${this.properties.context}.properties`,
|
||||
this._propertiesClass.collapsed ? 'collapsed' : true, ConfigurationTarget.USER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public layout() {
|
||||
super.layout();
|
||||
if (this._scrollable) {
|
||||
this._scrollable.layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-home-tab {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { ModelViewContent } from 'sql/workbench/browser/modelComponents/modelViewContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-modelview-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardModelViewContainer) }],
|
||||
template: `
|
||||
<modelview-content [modelViewId]="tab.id">
|
||||
</modelview-content>
|
||||
`
|
||||
})
|
||||
export class DashboardModelViewContainer extends DashboardTab implements AfterContentInit {
|
||||
@Input() private tab: TabConfig;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._modelViewContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._modelViewContent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const MODELVIEW_CONTAINER = 'modelview-container';
|
||||
|
||||
let modelviewSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('dashboard.container.modelview', "The model-backed view that will be displayed in this tab."),
|
||||
default: null
|
||||
};
|
||||
|
||||
registerContainerType(MODELVIEW_CONTAINER, modelviewSchema);
|
||||
registerNavSectionContainerType(MODELVIEW_CONTAINER, modelviewSchema);
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-modelview-container {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<panel [options]="panelOpt" style="flex: 1 1 auto;" class="dashboard-panel">
|
||||
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize"
|
||||
[identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions" [iconClass]="tab.iconClass">
|
||||
<ng-template>
|
||||
<dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab">
|
||||
</dashboard-webview-container>
|
||||
<dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab">
|
||||
</dashboard-widget-container>
|
||||
<dashboard-modelview-container *ngIf="getContentType(tab) === 'modelview-container'" [tab]="tab">
|
||||
</dashboard-modelview-container>
|
||||
<dashboard-grid-container *ngIf="getContentType(tab) === 'grid-container'" [tab]="tab">
|
||||
</dashboard-grid-container>
|
||||
<dashboard-error-container *ngIf="getContentType(tab) === 'error-container'" [tab]="tab">
|
||||
</dashboard-error-container>
|
||||
</ng-template>
|
||||
</tab>
|
||||
</panel>
|
||||
@@ -0,0 +1,181 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardNavSection';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, OnChanges, AfterContentInit } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface, SingleConnectionManagementService } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { WidgetConfig, TabConfig, NavSectionConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
import { DashboardTab, IConfigModifierCollection } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { WIDGETS_CONTAINER } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.contribution';
|
||||
import * as dashboardHelper from 'sql/workbench/contrib/dashboard/browser/core/dashboardHelper';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-nav-section',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardNavSection) }],
|
||||
templateUrl: decodeURI(require.toUrl('./dashboardNavSection.component.html'))
|
||||
})
|
||||
export class DashboardNavSection extends DashboardTab implements OnDestroy, OnChanges, AfterContentInit, IConfigModifierCollection {
|
||||
@Input() private tab: TabConfig;
|
||||
protected tabs: Array<TabConfig> = [];
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private readonly panelOpt: IPanelOptions = {
|
||||
layout: NavigationBarLayout.vertical
|
||||
};
|
||||
|
||||
// a set of config modifiers
|
||||
private readonly _configModifiers: Array<(item: Array<WidgetConfig>, collection: IConfigModifierCollection, context: string) => Array<WidgetConfig>> = [
|
||||
dashboardHelper.removeEmpty,
|
||||
dashboardHelper.initExtensionConfigs,
|
||||
dashboardHelper.addProvider,
|
||||
dashboardHelper.addEdition,
|
||||
dashboardHelper.addContext,
|
||||
dashboardHelper.filterConfigs
|
||||
];
|
||||
|
||||
private readonly _gridModifiers: Array<(item: Array<WidgetConfig>, originalConfig: Array<WidgetConfig>) => Array<WidgetConfig>> = [
|
||||
dashboardHelper.validateGridConfig
|
||||
];
|
||||
|
||||
@ViewChildren(TabChild) private _tabs: QueryList<DashboardTab>;
|
||||
@ViewChild(PanelComponent) private _panel: PanelComponent;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef,
|
||||
@Inject(ILogService) private logService: ILogService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.tabs = [];
|
||||
let navSectionContainers: NavSectionConfig[] = [];
|
||||
if (this.tab.container) {
|
||||
navSectionContainers = values(this.tab.container)[0];
|
||||
let hasIcon = true;
|
||||
navSectionContainers.forEach(navSection => {
|
||||
if (!navSection.iconClass) {
|
||||
hasIcon = false;
|
||||
}
|
||||
});
|
||||
this.panelOpt.showIcon = hasIcon;
|
||||
this.loadNewTabs(navSectionContainers);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
this._register(tabContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private loadNewTabs(dashboardTabs: NavSectionConfig[]) {
|
||||
if (dashboardTabs && dashboardTabs.length > 0) {
|
||||
dashboardTabs.map(v => {
|
||||
const containerResult = dashboardHelper.getDashboardContainer(v.container, this.logService);
|
||||
if (!containerResult.result) {
|
||||
return { id: v.id, title: v.title, container: { 'error-container': undefined } };
|
||||
}
|
||||
|
||||
const key = Object.keys(containerResult.container)[0];
|
||||
if (key === WIDGETS_CONTAINER || key === GRID_CONTAINER) {
|
||||
let configs = <WidgetConfig[]>values(containerResult.container)[0];
|
||||
this._configModifiers.forEach(cb => {
|
||||
configs = cb.apply(this, [configs, this, this.tab.context]);
|
||||
});
|
||||
this._gridModifiers.forEach(cb => {
|
||||
configs = cb.apply(this, [configs]);
|
||||
});
|
||||
if (key === WIDGETS_CONTAINER) {
|
||||
return { id: v.id, title: v.title, container: { 'widgets-container': configs }, iconClass: v.iconClass };
|
||||
|
||||
} else {
|
||||
return { id: v.id, title: v.title, container: { 'grid-container': configs }, iconClass: v.iconClass };
|
||||
}
|
||||
}
|
||||
return { id: v.id, title: v.title, container: containerResult.container, iconClass: v.iconClass };
|
||||
}).map(v => {
|
||||
const config = v as TabConfig;
|
||||
config.context = this.tab.context;
|
||||
config.editable = false;
|
||||
config.canClose = false;
|
||||
this.addNewTab(config);
|
||||
return config;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addNewTab(tab: TabConfig): void {
|
||||
const existedTab = find(this.tabs, i => i.id === tab.id);
|
||||
if (!existedTab) {
|
||||
this.tabs.push(tab);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
protected getContentType(tab: TabConfig): string {
|
||||
return tab.container ? Object.keys(tab.container)[0] : '';
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
const activeTabId = this._panel.getActiveTab;
|
||||
const localtab = this._tabs.find(i => i.id === activeTabId);
|
||||
this._cd.detectChanges();
|
||||
localtab.layout();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
tabContent.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
if (this._tabs) {
|
||||
this._tabs.forEach(tabContent => {
|
||||
tabContent.enableEdit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get connectionManagementService(): SingleConnectionManagementService {
|
||||
return this.dashboardService.connectionManagementService;
|
||||
}
|
||||
|
||||
public get contextKeyService(): IContextKeyService {
|
||||
return this.dashboardService.scopedContextKeyService;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
|
||||
import { IdGenerator } from 'vs/base/common/idGenerator';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
|
||||
import { NavSectionConfig, IUserFriendlyIcon } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardWidgetContainer.contribution';
|
||||
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.contribution';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
|
||||
export const NAV_SECTION = 'nav-section';
|
||||
|
||||
const navSectionContainerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.container.left-nav-bar.id', "Unique identifier for this nav section. Will be passed to the extension for any requests.")
|
||||
},
|
||||
icon: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.icon', "(Optional) Icon which is used to represent this nav section in the UI. Either a file path or a themeable configuration"),
|
||||
anyOf: [{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.icon.light', "Icon path when a light theme is used"),
|
||||
type: 'string'
|
||||
},
|
||||
dark: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.icon.dark', "Icon path when a dark theme is used"),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}]
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: nls.localize('dashboard.container.left-nav-bar.title', "Title of the nav section to show the user.")
|
||||
},
|
||||
container: {
|
||||
description: nls.localize('dashboard.container.left-nav-bar.container', "The container that will be displayed in this nav section."),
|
||||
type: 'object',
|
||||
properties: generateNavSectionContainerTypeSchemaProperties()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const NavSectionSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.container.left-nav-bar', "The list of dashboard containers that will be displayed in this navigation section."),
|
||||
items: navSectionContainerSchema
|
||||
};
|
||||
|
||||
registerContainerType(NAV_SECTION, NavSectionSchema);
|
||||
|
||||
function isValidIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): boolean {
|
||||
if (typeof icon === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
if (typeof icon === 'string') {
|
||||
return true;
|
||||
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
|
||||
return true;
|
||||
}
|
||||
extension.collector.error(nls.localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const ids = new IdGenerator('contrib-dashboardNavSection-icon-');
|
||||
|
||||
function createCSSRuleForIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): string {
|
||||
let iconClass: string;
|
||||
if (icon) {
|
||||
iconClass = ids.nextId();
|
||||
if (typeof icon === 'string') {
|
||||
const path = resources.joinPath(extension.description.extensionLocation, icon);
|
||||
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(path)}`);
|
||||
} else {
|
||||
const light = resources.joinPath(extension.description.extensionLocation, icon.light);
|
||||
const dark = resources.joinPath(extension.description.extensionLocation, icon.dark);
|
||||
createCSSRule(`.codicon.${iconClass}`, `background-image: ${asCSSUrl(light)}`);
|
||||
createCSSRule(`.vs-dark .codicon.${iconClass}, .hc-black .codicon.${iconClass}`, `background-image: ${asCSSUrl(dark)}`);
|
||||
}
|
||||
}
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
export function validateNavSectionContributionAndRegisterIcon(extension: IExtensionPointUser<any>, navSectionConfigs: NavSectionConfig[]): boolean {
|
||||
let result = true;
|
||||
navSectionConfigs.forEach(section => {
|
||||
if (!section.title) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.missingTitle.error', "No title in nav section specified for extension."));
|
||||
}
|
||||
|
||||
if (!section.container) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.missingContainer.error', "No container in nav section specified for extension."));
|
||||
}
|
||||
|
||||
if (Object.keys(section.container).length !== 1) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.moreThanOneDashboardContainersError', "Exactly 1 dashboard container must be defined per space."));
|
||||
}
|
||||
|
||||
if (isValidIcon(section.icon, extension)) {
|
||||
section.iconClass = createCSSRuleForIcon(section.icon, extension);
|
||||
}
|
||||
|
||||
const containerKey = Object.keys(section.container)[0];
|
||||
const containerValue = values(section.container)[0];
|
||||
|
||||
switch (containerKey) {
|
||||
case WIDGETS_CONTAINER:
|
||||
result = result && validateWidgetContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case GRID_CONTAINER:
|
||||
result = result && validateGridContainerContribution(extension, containerValue);
|
||||
break;
|
||||
case NAV_SECTION:
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('navSection.invalidContainer.error', "NAV_SECTION within NAV_SECTION is an invalid container for extension."));
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-nav-section {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardWebviewContainer';
|
||||
|
||||
import { Component, forwardRef, Input, AfterContentInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { TabConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { WebviewContent } from 'sql/workbench/contrib/dashboard/browser/contents/webviewContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-webview-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWebviewContainer) }],
|
||||
template: `
|
||||
<webview-content [webviewId]="tab.id">
|
||||
</webview-content>
|
||||
`
|
||||
})
|
||||
export class DashboardWebviewContainer extends DashboardTab implements AfterContentInit {
|
||||
@Input() private tab: TabConfig;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(WebviewContent) private _webviewContent: WebviewContent;
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._webviewContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._webviewContent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// no op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
|
||||
export const WEBVIEW_CONTAINER = 'webview-container';
|
||||
|
||||
let webviewSchema: IJSONSchema = {
|
||||
type: 'null',
|
||||
description: nls.localize('dashboard.container.webview', "The webview that will be displayed in this tab."),
|
||||
default: null
|
||||
};
|
||||
|
||||
registerContainerType(WEBVIEW_CONTAINER, webviewSchema);
|
||||
registerNavSectionContainerType(WEBVIEW_CONTAINER, webviewSchema);
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-webview-container {
|
||||
height: 100%;
|
||||
width : 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./dashboardWidgetContainer';
|
||||
|
||||
import { Component, Inject, Input, forwardRef, ViewChild, OnDestroy, ChangeDetectorRef, AfterContentInit } from '@angular/core';
|
||||
|
||||
import { TabConfig, WidgetConfig } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget';
|
||||
import { DashboardTab } from 'sql/workbench/contrib/dashboard/browser/core/interfaces';
|
||||
import { WidgetContent } from 'sql/workbench/contrib/dashboard/browser/contents/widgetContent.component';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
|
||||
@Component({
|
||||
selector: 'dashboard-widget-container',
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardWidgetContainer) }],
|
||||
template: `
|
||||
<widget-content [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
|
||||
</widget-content>
|
||||
`
|
||||
})
|
||||
export class DashboardWidgetContainer extends DashboardTab implements OnDestroy, AfterContentInit {
|
||||
@Input() protected tab: TabConfig;
|
||||
protected widgets: WidgetConfig[];
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
@ViewChild(WidgetContent) protected _widgetContent: WidgetContent;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) protected _cd: ChangeDetectorRef
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.tab.container) {
|
||||
this.widgets = values(this.tab.container)[0];
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._register(this._widgetContent.onResize(() => {
|
||||
this._onResize.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.tab.id;
|
||||
}
|
||||
|
||||
public get editable(): boolean {
|
||||
return this.tab.editable;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
this._widgetContent.layout();
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this._widgetContent.refresh();
|
||||
}
|
||||
|
||||
public enableEdit(): void {
|
||||
this._widgetContent.enableEdit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { generateDashboardWidgetSchema } from 'sql/workbench/contrib/dashboard/browser/pages/dashboardPageContribution';
|
||||
import { registerContainerType, registerNavSectionContainerType } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
|
||||
export const WIDGETS_CONTAINER = 'widgets-container';
|
||||
|
||||
const widgetsSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localize('dashboard.container.widgets', "The list of widgets that will be displayed in this tab."),
|
||||
items: generateDashboardWidgetSchema(undefined, true)
|
||||
};
|
||||
|
||||
registerContainerType(WIDGETS_CONTAINER, widgetsSchema);
|
||||
registerNavSectionContainerType(WIDGETS_CONTAINER, widgetsSchema);
|
||||
|
||||
export function validateWidgetContainerContribution(extension: IExtensionPointUser<any>, WidgetConfigs: object[]): boolean {
|
||||
let result = true;
|
||||
WidgetConfigs.forEach(widgetConfig => {
|
||||
const allKeys = Object.keys(widgetConfig);
|
||||
const widgetKey = find(allKeys, key => key === 'widget');
|
||||
if (!widgetKey) {
|
||||
result = false;
|
||||
extension.collector.error(nls.localize('widgetContainer.invalidInputs', "The list of widgets is expected inside widgets-container for extension."));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
dashboard-widget-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
|
||||
<agentview-component #agent *ngIf="(controlType) === 'agent'"></agentview-component>
|
||||
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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!./controlHostContent';
|
||||
|
||||
import { Component, forwardRef, Input, Inject, ChangeDetectorRef, ViewChild } from '@angular/core';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { AgentViewComponent } from '../../../jobManagement/browser/agentView.component';
|
||||
|
||||
@Component({
|
||||
templateUrl: decodeURI(require.toUrl('./controlHostContent.component.html')),
|
||||
selector: 'controlhost-content'
|
||||
})
|
||||
export class ControlHostContent {
|
||||
@Input() private webviewId: string;
|
||||
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
private _onMessage = new Emitter<string>();
|
||||
public readonly onMessage: Event<string> = this._onMessage.event;
|
||||
|
||||
private _type: string;
|
||||
|
||||
/* Children components */
|
||||
@ViewChild('agent') private _agentViewComponent: AgentViewComponent;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
|
||||
) {
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._agentViewComponent.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.webviewId;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get connection(): azdata.connection.Connection {
|
||||
const currentConnection = this._dashboardService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
const connection: azdata.connection.Connection = {
|
||||
providerName: currentConnection.providerName,
|
||||
connectionId: currentConnection.id,
|
||||
options: currentConnection.options
|
||||
};
|
||||
return connection;
|
||||
}
|
||||
|
||||
@memoize
|
||||
public get serverInfo(): azdata.ServerInfo {
|
||||
return this._dashboardService.connectionManagementService.connectionInfo.serverInfo;
|
||||
}
|
||||
|
||||
public setControlType(type: string): void {
|
||||
this._type = type;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
public get controlType(): string {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this._agentViewComponent.refresh = true;
|
||||
}
|
||||
}
|
||||