mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-23 01:25:38 -05:00
Another code layering (#4037)
* working on formatting * fixed basic lint errors; starting moving things to their appropriate location * formatting * update tslint to match the version of vscode we have * remove unused code * work in progress fixing layering * formatting * moved connection management service to platform * formatting * add missing file * moving more servies * formatting * moving more services * formatting * wip * moving more services * formatting * move css file * add missing svgs * moved the rest of services * formatting * changing around some references * formatting * revert tslint * revert some changes that brake things * formatting * fix tests * fix testzx * fix tests * fix tests * fix compile issue
This commit is contained in:
130
src/sql/workbench/services/backup/browser/backupUiService.ts
Normal file
130
src/sql/workbench/services/backup/browser/backupUiService.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import * as ConnectionUtils from 'sql/platform/connection/common/utils';
|
||||
import { ProviderConnectionInfo } from 'sql/platform/connection/common/providerConnectionInfo';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { BackupDialog } from 'sql/parts/disasterRecovery/backup/backupDialog';
|
||||
import { OptionsDialog } from 'sql/workbench/browser/modal/optionsDialog';
|
||||
import { IBackupService, TaskExecutionMode } from 'sql/platform/backup/common/backupService';
|
||||
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
|
||||
|
||||
export class BackupUiService implements IBackupUiService {
|
||||
public _serviceBrand: any;
|
||||
private _backupDialogs: { [providerName: string]: BackupDialog | OptionsDialog } = {};
|
||||
private _currentProvider: string;
|
||||
private _optionValues: { [optionName: string]: any } = {};
|
||||
private _connectionUri: string;
|
||||
private static _connectionUniqueId: number = 0;
|
||||
|
||||
private _onShowBackupEvent: Emitter<{ connection: IConnectionProfile, ownerUri: string }>;
|
||||
public get onShowBackupEvent(): Event<{ connection: IConnectionProfile, ownerUri: string }> { return this._onShowBackupEvent.event; }
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||
@IBackupService private _disasterRecoveryService: IBackupService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
this._onShowBackupEvent = new Emitter<{ connection: IConnectionProfile, ownerUri: string }>();
|
||||
}
|
||||
|
||||
public showBackup(connection: IConnectionProfile): Promise<any> {
|
||||
let self = this;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
self.showBackupDialog(connection).then(() => {
|
||||
resolve(void 0);
|
||||
}, error => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getOptions(provider: string): sqlops.ServiceOption[] {
|
||||
let feature = this._capabilitiesService.getLegacyCapabilities(this._currentProvider).features.find(f => f.featureName === 'backup');
|
||||
if (feature) {
|
||||
return feature.optionsMetadata;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public showBackupDialog(connection: IConnectionProfile): TPromise<void> {
|
||||
let self = this;
|
||||
self._connectionUri = ConnectionUtils.generateUri(connection);
|
||||
self._currentProvider = connection.providerName;
|
||||
let backupDialog = self._backupDialogs[self._currentProvider];
|
||||
if (!backupDialog) {
|
||||
let backupOptions = this.getOptions(this._currentProvider);
|
||||
if (backupOptions) {
|
||||
backupDialog = self._instantiationService ? self._instantiationService.createInstance(
|
||||
OptionsDialog, 'Backup database - ' + connection.serverName + ':' + connection.databaseName, 'BackupOptions', undefined) : undefined;
|
||||
backupDialog.onOk(() => this.handleOptionDialogClosed());
|
||||
}
|
||||
else {
|
||||
backupDialog = self._instantiationService ? self._instantiationService.createInstance(BackupDialog) : undefined;
|
||||
}
|
||||
backupDialog.render();
|
||||
self._backupDialogs[self._currentProvider] = backupDialog;
|
||||
}
|
||||
|
||||
let backupOptions = this.getOptions(this._currentProvider);
|
||||
return new TPromise<void>((resolve) => {
|
||||
let uri = this._connectionManagementService.getConnectionUri(connection)
|
||||
+ ProviderConnectionInfo.idSeparator
|
||||
+ ConnectionUtils.ConnectionUriBackupIdAttributeName
|
||||
+ ProviderConnectionInfo.nameValueSeparator
|
||||
+ BackupUiService._connectionUniqueId;
|
||||
|
||||
this._connectionUri = uri;
|
||||
|
||||
BackupUiService._connectionUniqueId++;
|
||||
|
||||
// Create connection if needed
|
||||
if (!this._connectionManagementService.isConnected(uri)) {
|
||||
this._connectionManagementService.connect(connection, uri).then(() => {
|
||||
this._onShowBackupEvent.fire({ connection: connection, ownerUri: uri });
|
||||
});
|
||||
}
|
||||
|
||||
if (backupOptions) {
|
||||
(backupDialog as OptionsDialog).open(backupOptions, self._optionValues);
|
||||
} else {
|
||||
(backupDialog as BackupDialog).open(connection);
|
||||
}
|
||||
resolve(void 0);
|
||||
});
|
||||
}
|
||||
|
||||
public onShowBackupDialog() {
|
||||
let backupDialog = this._backupDialogs[this._currentProvider];
|
||||
if (backupDialog) {
|
||||
backupDialog.setFocusableElements();
|
||||
}
|
||||
}
|
||||
|
||||
public closeBackup() {
|
||||
let self = this;
|
||||
let backupDialog = self._backupDialogs[self._currentProvider];
|
||||
if (backupDialog) {
|
||||
backupDialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
private handleOptionDialogClosed() {
|
||||
this._disasterRecoveryService.backup(this._connectionUri, this._optionValues, TaskExecutionMode.executeAndScript);
|
||||
}
|
||||
|
||||
}
|
||||
37
src/sql/workbench/services/backup/common/backupUiService.ts
Normal file
37
src/sql/workbench/services/backup/common/backupUiService.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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export const UI_SERVICE_ID = 'backupUiService';
|
||||
export const IBackupUiService = createDecorator<IBackupUiService>(UI_SERVICE_ID);
|
||||
|
||||
export interface IBackupUiService {
|
||||
_serviceBrand: any;
|
||||
|
||||
/**
|
||||
* Show backup wizard
|
||||
*/
|
||||
showBackup(connection: IConnectionProfile): Promise<any>;
|
||||
|
||||
/**
|
||||
* On show backup event
|
||||
*/
|
||||
onShowBackupEvent: Event<{ connection: IConnectionProfile, ownerUri: string }>;
|
||||
|
||||
/**
|
||||
* Close backup wizard
|
||||
*/
|
||||
closeBackup();
|
||||
|
||||
/**
|
||||
* After the backup dialog is rendered, run Modal methods to set focusable elements, etc.
|
||||
*/
|
||||
onShowBackupDialog();
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con
|
||||
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 { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import * as platform from 'vs/platform/registry/common/platform';
|
||||
import { IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionComponentCallbacks, IConnectionComponentController, IConnectionValidateResult } from 'sql/workbench/services/connection/browser/connectionDialogService';
|
||||
import { AdvancedPropertiesController } from 'sql/parts/connection/connectionDialog/advancedPropertiesController';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as Utils from 'sql/platform/connection/common/utils';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { ConnectionProviderProperties } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
|
||||
import { ConnectionWidget } from 'sql/workbench/services/connection/browser/connectionWidget';
|
||||
|
||||
export class ConnectionController implements IConnectionComponentController {
|
||||
private _container: HTMLElement;
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
private _callback: IConnectionComponentCallbacks;
|
||||
private _connectionWidget: ConnectionWidget;
|
||||
private _advancedController: AdvancedPropertiesController;
|
||||
private _model: IConnectionProfile;
|
||||
private _providerOptions: sqlops.ConnectionOption[];
|
||||
private _providerName: string;
|
||||
/* key: uri, value : list of databases */
|
||||
private _databaseCache = new Map<string, string[]>();
|
||||
|
||||
constructor(container: HTMLElement,
|
||||
connectionManagementService: IConnectionManagementService,
|
||||
connectionProperties: ConnectionProviderProperties,
|
||||
callback: IConnectionComponentCallbacks,
|
||||
providerName: string,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService) {
|
||||
this._container = container;
|
||||
this._connectionManagementService = connectionManagementService;
|
||||
this._callback = callback;
|
||||
this._providerOptions = connectionProperties.connectionOptions;
|
||||
var specialOptions = this._providerOptions.filter(
|
||||
(property) => (property.specialValueType !== null && property.specialValueType !== undefined));
|
||||
this._connectionWidget = this._instantiationService.createInstance(ConnectionWidget, specialOptions, {
|
||||
onSetConnectButton: (enable: boolean) => this._callback.onSetConnectButton(enable),
|
||||
onCreateNewServerGroup: () => this.onCreateNewServerGroup(),
|
||||
onAdvancedProperties: () => this.handleOnAdvancedProperties(),
|
||||
onSetAzureTimeOut: () => this.handleonSetAzureTimeOut(),
|
||||
onFetchDatabases: (serverName: string, authenticationType: string, userName?: string, password?: string) => this.onFetchDatabases(
|
||||
serverName, authenticationType, userName, password).then(result => {
|
||||
return result;
|
||||
})
|
||||
}, providerName);
|
||||
this._providerName = providerName;
|
||||
}
|
||||
|
||||
private onFetchDatabases(serverName: string, authenticationType: string, userName?: string, password?: string): Promise<string[]> {
|
||||
let tempProfile = this._model;
|
||||
tempProfile.serverName = serverName;
|
||||
tempProfile.authenticationType = authenticationType;
|
||||
tempProfile.userName = userName;
|
||||
tempProfile.password = password;
|
||||
tempProfile.groupFullName = '';
|
||||
tempProfile.saveProfile = false;
|
||||
let uri = this._connectionManagementService.getConnectionUri(tempProfile);
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
if (this._databaseCache.has(uri)) {
|
||||
let cachedDatabases: string[] = this._databaseCache.get(uri);
|
||||
if (cachedDatabases !== null) {
|
||||
resolve(cachedDatabases);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
} else {
|
||||
this._connectionManagementService.connect(tempProfile, uri).then(connResult => {
|
||||
if (connResult && connResult.connected) {
|
||||
this._connectionManagementService.listDatabases(uri).then(result => {
|
||||
if (result && result.databaseNames) {
|
||||
this._databaseCache.set(uri, result.databaseNames);
|
||||
resolve(result.databaseNames);
|
||||
} else {
|
||||
this._databaseCache.set(uri, null);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(connResult.errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onCreateNewServerGroup(): void {
|
||||
this._connectionManagementService.showCreateServerGroupDialog({
|
||||
onAddGroup: (groupName) => this._connectionWidget.updateServerGroup(this.getAllServerGroups(), groupName),
|
||||
onClose: () => this._connectionWidget.focusOnServerGroup()
|
||||
});
|
||||
}
|
||||
|
||||
private handleonSetAzureTimeOut(): void {
|
||||
var timeoutPropertyName = 'connectTimeout';
|
||||
var timeoutOption = this._model.options[timeoutPropertyName];
|
||||
if (timeoutOption === undefined || timeoutOption === null) {
|
||||
this._model.options[timeoutPropertyName] = 30;
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnAdvancedProperties(): void {
|
||||
if (!this._advancedController) {
|
||||
this._advancedController = this._instantiationService.createInstance(AdvancedPropertiesController, () => this._connectionWidget.focusOnAdvancedButton());
|
||||
}
|
||||
var advancedOption = this._providerOptions.filter(
|
||||
(property) => (property.specialValueType === undefined || property.specialValueType === null));
|
||||
this._advancedController.showDialog(advancedOption, this._container, this._model.options);
|
||||
}
|
||||
|
||||
public showUiComponent(container: HTMLElement): void {
|
||||
this._databaseCache = new Map<string, string[]>();
|
||||
this._connectionWidget.createConnectionWidget(container);
|
||||
}
|
||||
|
||||
private getServerGroupHelper(group: ConnectionProfileGroup, groupNames: IConnectionProfileGroup[]): void {
|
||||
if (group) {
|
||||
if (group.fullName !== '') {
|
||||
groupNames.push(group);
|
||||
}
|
||||
if (group.hasChildren()) {
|
||||
group.children.forEach((child) => this.getServerGroupHelper(child, groupNames));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getAllServerGroups(providers?: string[]): IConnectionProfileGroup[] {
|
||||
var connectionGroupRoot = this._connectionManagementService.getConnectionGroups(providers);
|
||||
var connectionGroupNames: IConnectionProfileGroup[] = [];
|
||||
if (connectionGroupRoot && connectionGroupRoot.length > 0) {
|
||||
this.getServerGroupHelper(connectionGroupRoot[0], connectionGroupNames);
|
||||
}
|
||||
let defaultGroupId: string;
|
||||
if (connectionGroupRoot && connectionGroupRoot.length > 0 && ConnectionProfileGroup.isRoot(connectionGroupRoot[0].name)) {
|
||||
defaultGroupId = connectionGroupRoot[0].id;
|
||||
} else {
|
||||
defaultGroupId = Utils.defaultGroupId;
|
||||
}
|
||||
connectionGroupNames.push(Object.assign({}, this._connectionWidget.DefaultServerGroup, { id: defaultGroupId }));
|
||||
connectionGroupNames.push(this._connectionWidget.NoneServerGroup);
|
||||
return connectionGroupNames;
|
||||
}
|
||||
|
||||
public initDialog(providers: string[], connectionInfo: IConnectionProfile): void {
|
||||
this._connectionWidget.updateServerGroup(this.getAllServerGroups(providers));
|
||||
this._model = connectionInfo;
|
||||
this._model.providerName = this._providerName;
|
||||
let appNameOption = this._providerOptions.find(option => option.specialValueType === ConnectionOptionSpecialType.appName);
|
||||
if (appNameOption) {
|
||||
let appNameKey = appNameOption.name;
|
||||
this._model.options[appNameKey] = Constants.applicationName;
|
||||
}
|
||||
this._connectionWidget.initDialog(this._model);
|
||||
}
|
||||
|
||||
public focusOnOpen(): void {
|
||||
this._connectionWidget.focusOnOpen();
|
||||
}
|
||||
|
||||
public validateConnection(): IConnectionValidateResult {
|
||||
return { isValid: this._connectionWidget.connect(this._model), connection: this._model };
|
||||
}
|
||||
|
||||
public fillInConnectionInputs(connectionInfo: IConnectionProfile): void {
|
||||
this._model = connectionInfo;
|
||||
this._connectionWidget.fillInConnectionInputs(connectionInfo);
|
||||
}
|
||||
|
||||
public handleOnConnecting(): void {
|
||||
this._connectionWidget.handleOnConnecting();
|
||||
}
|
||||
|
||||
public handleResetConnection(): void {
|
||||
this._connectionWidget.handleResetConnection();
|
||||
}
|
||||
|
||||
public closeDatabaseDropdown(): void {
|
||||
this._connectionWidget.closeDatabaseDropdown();
|
||||
}
|
||||
|
||||
public get databaseDropdownExpanded(): boolean {
|
||||
return this._connectionWidget.databaseDropdownExpanded;
|
||||
}
|
||||
|
||||
public set databaseDropdownExpanded(val: boolean) {
|
||||
this._connectionWidget.databaseDropdownExpanded = val;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
IConnectionManagementService,
|
||||
ConnectionType, INewConnectionParams, IConnectionCompletionOptions, IConnectionResult
|
||||
} from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ConnectionDialogWidget, OnShowUIResponse } from 'sql/workbench/services/connection/browser/connectionDialogWidget';
|
||||
import { ConnectionController } from 'sql/workbench/services/connection/browser/connectionController';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { entries } from 'sql/base/common/objects';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
|
||||
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { trim } from 'vs/base/common/strings';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export interface IConnectionValidateResult {
|
||||
isValid: boolean;
|
||||
connection: IConnectionProfile;
|
||||
}
|
||||
|
||||
export interface IConnectionComponentCallbacks {
|
||||
onSetConnectButton: (enable: boolean) => void;
|
||||
onCreateNewServerGroup?: () => void;
|
||||
onAdvancedProperties?: () => void;
|
||||
onSetAzureTimeOut?: () => void;
|
||||
onFetchDatabases?: (serverName: string, authenticationType: string, userName?: string, password?: string) => Promise<string[]>;
|
||||
}
|
||||
|
||||
export interface IConnectionComponentController {
|
||||
showUiComponent(container: HTMLElement): void;
|
||||
initDialog(providers: string[], model: IConnectionProfile): void;
|
||||
validateConnection(): IConnectionValidateResult;
|
||||
fillInConnectionInputs(connectionInfo: IConnectionProfile): void;
|
||||
handleOnConnecting(): void;
|
||||
handleResetConnection(): void;
|
||||
focusOnOpen(): void;
|
||||
closeDatabaseDropdown(): void;
|
||||
databaseDropdownExpanded: boolean;
|
||||
}
|
||||
|
||||
export class ConnectionDialogService implements IConnectionDialogService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private _container: HTMLElement;
|
||||
private _connectionDialog: ConnectionDialogWidget;
|
||||
private _connectionControllerMap: { [providerDisplayName: string]: IConnectionComponentController } = {};
|
||||
private _model: ConnectionProfile;
|
||||
private _params: INewConnectionParams;
|
||||
private _inputModel: IConnectionProfile;
|
||||
private _providerNameToDisplayNameMap: { [providerDisplayName: string]: string } = {};
|
||||
private _providerTypes: string[] = [];
|
||||
private _currentProviderType: string = 'Microsoft SQL Server';
|
||||
private _connecting: boolean = false;
|
||||
private _connectionErrorTitle = localize('connectionError', 'Connection error');
|
||||
private _dialogDeferredPromise: Deferred<IConnectionProfile>;
|
||||
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
|
||||
constructor(
|
||||
@IPartService private _partService: IPartService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService,
|
||||
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@ICommandService private _commandService: ICommandService
|
||||
) { }
|
||||
/**
|
||||
* Gets the default provider with the following actions
|
||||
* 1. Checks if master provider(map) has data
|
||||
* 2. If so, filters provider paramter against master map
|
||||
* 3. Fetches the result array and extracts the first element
|
||||
* 4. If none of the above data exists, returns 'MSSQL'
|
||||
* @returns: Default provider as string
|
||||
*/
|
||||
private getDefaultProviderName(): string {
|
||||
let defaultProvider: string;
|
||||
if (this._providerNameToDisplayNameMap) {
|
||||
let keys = Object.keys(this._providerNameToDisplayNameMap);
|
||||
let filteredKeys: string[];
|
||||
if (keys && keys.length > 0) {
|
||||
if (this._params && this._params.providers && this._params.providers.length > 0) {
|
||||
//Filter providers from master keys.
|
||||
filteredKeys = keys.filter(key => this._params.providers.includes(key));
|
||||
}
|
||||
if (filteredKeys && filteredKeys.length > 0) {
|
||||
defaultProvider = filteredKeys[0];
|
||||
}
|
||||
else {
|
||||
defaultProvider = keys[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!defaultProvider && this._workspaceConfigurationService) {
|
||||
defaultProvider = WorkbenchUtils.getSqlConfigValue<string>(this._workspaceConfigurationService, Constants.defaultEngine);
|
||||
}
|
||||
// as a fallback, default to MSSQL if the value from settings is not available
|
||||
return defaultProvider || Constants.mssqlProviderName;
|
||||
}
|
||||
|
||||
private handleOnConnect(params: INewConnectionParams, profile?: IConnectionProfile): void {
|
||||
if (!this._connecting) {
|
||||
this._connecting = true;
|
||||
this.handleProviderOnConnecting();
|
||||
if (!profile) {
|
||||
let result = this.uiController.validateConnection();
|
||||
if (!result.isValid) {
|
||||
this._connecting = false;
|
||||
this._connectionDialog.resetConnection();
|
||||
return;
|
||||
}
|
||||
profile = result.connection;
|
||||
|
||||
profile.serverName = trim(profile.serverName);
|
||||
|
||||
// append the port to the server name for SQL Server connections
|
||||
if (this.getCurrentProviderName() === Constants.mssqlProviderName) {
|
||||
let portPropertyName: string = 'port';
|
||||
let portOption: string = profile.options[portPropertyName];
|
||||
if (portOption && portOption.indexOf(',') === -1) {
|
||||
profile.serverName = profile.serverName + ',' + portOption;
|
||||
}
|
||||
profile.options[portPropertyName] = undefined;
|
||||
}
|
||||
|
||||
// Disable password prompt during reconnect if connected with an empty password
|
||||
if (profile.password === '' && profile.savePassword === false) {
|
||||
profile.savePassword = true;
|
||||
}
|
||||
|
||||
this.handleDefaultOnConnect(params, profile);
|
||||
} else {
|
||||
profile.serverName = trim(profile.serverName);
|
||||
this._connectionManagementService.addSavedPassword(profile).then(connectionWithPassword => {
|
||||
this.handleDefaultOnConnect(params, connectionWithPassword);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnCancel(params: INewConnectionParams): void {
|
||||
if (this.uiController.databaseDropdownExpanded) {
|
||||
this.uiController.closeDatabaseDropdown();
|
||||
} else {
|
||||
if (params && params.input && params.connectionType === ConnectionType.editor) {
|
||||
this._connectionManagementService.cancelEditorConnection(params.input);
|
||||
} else {
|
||||
this._connectionManagementService.cancelConnection(this._model);
|
||||
}
|
||||
if (params && params.input && params.input.onConnectCanceled) {
|
||||
params.input.onConnectCanceled();
|
||||
}
|
||||
this._connectionDialog.resetConnection();
|
||||
this._connecting = false;
|
||||
}
|
||||
this.uiController.databaseDropdownExpanded = false;
|
||||
if (this._dialogDeferredPromise) {
|
||||
this._dialogDeferredPromise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDefaultOnConnect(params: INewConnectionParams, connection: IConnectionProfile): Thenable<void> {
|
||||
let fromEditor = params && params.connectionType === ConnectionType.editor;
|
||||
let uri: string = undefined;
|
||||
if (fromEditor && params && params.input) {
|
||||
uri = params.input.uri;
|
||||
}
|
||||
let options: IConnectionCompletionOptions = {
|
||||
params: params,
|
||||
saveTheConnection: !fromEditor,
|
||||
showDashboard: params && params.showDashboard !== undefined ? params.showDashboard : !fromEditor,
|
||||
showConnectionDialogOnError: false,
|
||||
showFirewallRuleOnError: true
|
||||
};
|
||||
|
||||
return this._connectionManagementService.connectAndSaveProfile(connection, uri, options, params && params.input).then(connectionResult => {
|
||||
this._connecting = false;
|
||||
if (connectionResult && connectionResult.connected) {
|
||||
this._connectionDialog.close();
|
||||
if (this._dialogDeferredPromise) {
|
||||
this._dialogDeferredPromise.resolve(connectionResult.connectionProfile);
|
||||
}
|
||||
} else if (connectionResult && connectionResult.errorHandled) {
|
||||
this._connectionDialog.resetConnection();
|
||||
} else {
|
||||
this._connectionDialog.resetConnection();
|
||||
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack);
|
||||
}
|
||||
}).catch(err => {
|
||||
this._connecting = false;
|
||||
this._connectionDialog.resetConnection();
|
||||
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, err);
|
||||
});
|
||||
}
|
||||
|
||||
private get uiController(): IConnectionComponentController {
|
||||
// Find the provider name from the selected provider type, or throw an error if it does not correspond to a known provider
|
||||
let providerName = this.getCurrentProviderName();
|
||||
if (!providerName) {
|
||||
throw Error('Invalid provider type');
|
||||
}
|
||||
|
||||
// Set the model name, initialize the controller if needed, and return the controller
|
||||
this._model.providerName = providerName;
|
||||
if (!this._connectionControllerMap[providerName]) {
|
||||
this._connectionControllerMap[providerName] = this._instantiationService.createInstance(ConnectionController, this._container, this._connectionManagementService, this._capabilitiesService.getCapabilities(providerName).connection, {
|
||||
onSetConnectButton: (enable: boolean) => this.handleSetConnectButtonEnable(enable)
|
||||
}, providerName);
|
||||
}
|
||||
return this._connectionControllerMap[providerName];
|
||||
}
|
||||
|
||||
private handleSetConnectButtonEnable(enable: boolean): void {
|
||||
this._connectionDialog.connectButtonState = enable;
|
||||
}
|
||||
|
||||
private handleShowUiComponent(input: OnShowUIResponse) {
|
||||
if (input.selectedProviderType) {
|
||||
this._currentProviderType = input.selectedProviderType;
|
||||
}
|
||||
this._model.providerName = this.getCurrentProviderName();
|
||||
|
||||
this._model = new ConnectionProfile(this._capabilitiesService, this._model);
|
||||
this.uiController.showUiComponent(input.container);
|
||||
}
|
||||
|
||||
private handleInitDialog() {
|
||||
this.uiController.initDialog(this._params && this._params.providers, this._model);
|
||||
}
|
||||
|
||||
private handleFillInConnectionInputs(connectionInfo: IConnectionProfile): void {
|
||||
this._connectionManagementService.addSavedPassword(connectionInfo).then(connectionWithPassword => {
|
||||
var model = this.createModel(connectionWithPassword);
|
||||
this._model = model;
|
||||
this.uiController.fillInConnectionInputs(model);
|
||||
});
|
||||
this._connectionDialog.updateProvider(this._providerNameToDisplayNameMap[connectionInfo.providerName]);
|
||||
}
|
||||
|
||||
private handleProviderOnResetConnection(): void {
|
||||
this.uiController.handleResetConnection();
|
||||
}
|
||||
|
||||
private handleProviderOnConnecting(): void {
|
||||
this.uiController.handleOnConnecting();
|
||||
}
|
||||
|
||||
private updateModelServerCapabilities(model: IConnectionProfile) {
|
||||
this._model = this.createModel(model);
|
||||
if (this._model.providerName) {
|
||||
this._currentProviderType = this._providerNameToDisplayNameMap[this._model.providerName];
|
||||
if (this._connectionDialog) {
|
||||
this._connectionDialog.updateProvider(this._currentProviderType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createModel(model: IConnectionProfile): ConnectionProfile {
|
||||
let defaultProvider = this.getDefaultProviderName();
|
||||
let providerName = model ? model.providerName : defaultProvider;
|
||||
providerName = providerName ? providerName : defaultProvider;
|
||||
let newProfile = new ConnectionProfile(this._capabilitiesService, model || providerName);
|
||||
newProfile.saveProfile = true;
|
||||
newProfile.generateNewId();
|
||||
// If connecting from a query editor set "save connection" to false
|
||||
if (this._params && this._params.input && this._params.connectionType === ConnectionType.editor) {
|
||||
newProfile.saveProfile = false;
|
||||
}
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
private showDialogWithModel(): TPromise<void> {
|
||||
return new TPromise<void>((resolve, reject) => {
|
||||
this.updateModelServerCapabilities(this._inputModel);
|
||||
this.doShowDialog(this._params);
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
public openDialogAndWait(
|
||||
connectionManagementService: IConnectionManagementService,
|
||||
params?: INewConnectionParams,
|
||||
model?: IConnectionProfile,
|
||||
connectionResult?: IConnectionResult): Thenable<IConnectionProfile> {
|
||||
this._dialogDeferredPromise = new Deferred<IConnectionProfile>();
|
||||
|
||||
this.showDialog(connectionManagementService, params,
|
||||
model,
|
||||
connectionResult).then(() => {
|
||||
}, error => {
|
||||
this._dialogDeferredPromise.reject(error);
|
||||
});
|
||||
return this._dialogDeferredPromise;
|
||||
}
|
||||
|
||||
public showDialog(
|
||||
connectionManagementService: IConnectionManagementService,
|
||||
params?: INewConnectionParams,
|
||||
model?: IConnectionProfile,
|
||||
connectionResult?: IConnectionResult): Thenable<void> {
|
||||
this._connectionManagementService = connectionManagementService;
|
||||
|
||||
this._params = params;
|
||||
this._inputModel = model;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// only create the provider maps first time the dialog gets called
|
||||
if (this._providerTypes.length === 0) {
|
||||
entries(this._capabilitiesService.providers).forEach(p => {
|
||||
this._providerTypes.push(p[1].connection.displayName);
|
||||
this._providerNameToDisplayNameMap[p[0]] = p[1].connection.displayName;
|
||||
});
|
||||
}
|
||||
this.updateModelServerCapabilities(model);
|
||||
// If connecting from a query editor set "save connection" to false
|
||||
if (params && params.input && params.connectionType === ConnectionType.editor) {
|
||||
this._model.saveProfile = false;
|
||||
}
|
||||
|
||||
resolve(this.showDialogWithModel().then(() => {
|
||||
if (connectionResult && connectionResult.errorMessage) {
|
||||
this.showErrorDialog(Severity.Error, this._connectionErrorTitle, connectionResult.errorMessage, connectionResult.callStack);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private doShowDialog(params: INewConnectionParams): TPromise<void> {
|
||||
if (!this._connectionDialog) {
|
||||
let container = document.getElementById(this._partService.getWorkbenchElementId()).parentElement;
|
||||
this._container = container;
|
||||
this._connectionDialog = this._instantiationService.createInstance(ConnectionDialogWidget, this._providerTypes, this._providerNameToDisplayNameMap[this._model.providerName], this._providerNameToDisplayNameMap);
|
||||
this._connectionDialog.onCancel(() => {
|
||||
this._connectionDialog.databaseDropdownExpanded = this.uiController.databaseDropdownExpanded;
|
||||
this.handleOnCancel(this._connectionDialog.newConnectionParams);
|
||||
});
|
||||
this._connectionDialog.onConnect((profile) => this.handleOnConnect(this._connectionDialog.newConnectionParams, profile));
|
||||
this._connectionDialog.onShowUiComponent((input) => this.handleShowUiComponent(input));
|
||||
this._connectionDialog.onInitDialog(() => this.handleInitDialog());
|
||||
this._connectionDialog.onFillinConnectionInputs((input) => this.handleFillInConnectionInputs(input));
|
||||
this._connectionDialog.onResetConnection(() => this.handleProviderOnResetConnection());
|
||||
this._connectionDialog.render();
|
||||
}
|
||||
this._connectionDialog.newConnectionParams = params;
|
||||
|
||||
return new TPromise<void>(() => {
|
||||
this._connectionDialog.open(this._connectionManagementService.getRecentConnections(params.providers).length > 0);
|
||||
this.uiController.focusOnOpen();
|
||||
});
|
||||
}
|
||||
|
||||
private getCurrentProviderName(): string {
|
||||
return Object.keys(this._providerNameToDisplayNameMap).find(providerName => {
|
||||
return this._currentProviderType === this._providerNameToDisplayNameMap[providerName];
|
||||
});
|
||||
}
|
||||
|
||||
private showErrorDialog(severity: Severity, headerTitle: string, message: string, messageDetails?: string): void {
|
||||
// Kerberos errors are currently very hard to understand, so adding handling of these to solve the common scenario
|
||||
// note that ideally we would have an extensible service to handle errors by error code and provider, but for now
|
||||
// this solves the most common "hard error" that we've noticed
|
||||
const helpLink = 'https://aka.ms/sqlopskerberos';
|
||||
let actions: IAction[] = [];
|
||||
if (!platform.isWindows && types.isString(message) && message.toLowerCase().includes('kerberos') && message.toLowerCase().includes('kinit')) {
|
||||
message = [
|
||||
localize('kerberosErrorStart', "Connection failed due to Kerberos error."),
|
||||
localize('kerberosHelpLink', "Help configuring Kerberos is available at {0}", helpLink),
|
||||
localize('kerberosKinit', "If you have previously connected you may need to re-run kinit.")
|
||||
].join('\r\n');
|
||||
actions.push(new Action('Kinit', 'Run kinit', null, true, () => {
|
||||
this._connectionDialog.close();
|
||||
this._clipboardService.writeText('kinit\r');
|
||||
this._commandService.executeCommand('workbench.action.terminal.focus').then(resolve => {
|
||||
// setTimeout to allow for terminal Instance to load.
|
||||
setTimeout(() => {
|
||||
return this._commandService.executeCommand('workbench.action.terminal.paste');
|
||||
}, 10);
|
||||
}).then(resolve => null, reject => null);
|
||||
return null;
|
||||
}));
|
||||
|
||||
}
|
||||
this._errorMessageService.showDialog(severity, headerTitle, message, messageDetails, actions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/connectionDialog';
|
||||
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { attachModalDialogStyler, attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { IConnectionManagementService, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement';
|
||||
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
||||
import { TreeCreationUtils } from 'sql/parts/objectExplorer/viewlet/treeCreationUtils';
|
||||
import { TreeUpdateUtils } from 'sql/parts/objectExplorer/viewlet/treeUpdateUtils';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel';
|
||||
import { RecentConnectionTreeController, RecentConnectionActionsProvider } from 'sql/parts/connection/connectionDialog/recentConnectionTreeController';
|
||||
import { SavedConnectionTreeController } from 'sql/parts/connection/connectionDialog/savedConnectionTreeController';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import { ClearRecentConnectionsAction } from 'sql/parts/connection/common/connectionActions';
|
||||
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Builder, $ } from 'vs/base/browser/builder';
|
||||
import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as styler from 'vs/platform/theme/common/styler';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
|
||||
export interface OnShowUIResponse {
|
||||
selectedProviderType: string;
|
||||
container: HTMLElement;
|
||||
}
|
||||
|
||||
export class ConnectionDialogWidget extends Modal {
|
||||
private _bodyBuilder: Builder;
|
||||
private _recentConnectionBuilder: Builder;
|
||||
private _noRecentConnectionBuilder: Builder;
|
||||
private _savedConnectionBuilder: Builder;
|
||||
private _noSavedConnectionBuilder: Builder;
|
||||
private _connectionDetailTitle: Builder;
|
||||
private _connectButton: Button;
|
||||
private _closeButton: Button;
|
||||
private _providerTypeSelectBox: SelectBox;
|
||||
private _newConnectionParams: INewConnectionParams;
|
||||
private _recentConnectionTree: ITree;
|
||||
private _savedConnectionTree: ITree;
|
||||
private $connectionUIContainer: Builder;
|
||||
private _databaseDropdownExpanded: boolean;
|
||||
private _actionbar: ActionBar;
|
||||
private _providers: string[];
|
||||
|
||||
private _panel: TabbedPanel;
|
||||
private _recentConnectionTabId: PanelTabIdentifier;
|
||||
|
||||
private _onInitDialog = new Emitter<void>();
|
||||
public onInitDialog: Event<void> = this._onInitDialog.event;
|
||||
|
||||
private _onCancel = new Emitter<void>();
|
||||
public onCancel: Event<void> = this._onCancel.event;
|
||||
|
||||
private _onConnect = new Emitter<IConnectionProfile>();
|
||||
public onConnect: Event<IConnectionProfile> = this._onConnect.event;
|
||||
|
||||
private _onShowUiComponent = new Emitter<OnShowUIResponse>();
|
||||
public onShowUiComponent: Event<OnShowUIResponse> = this._onShowUiComponent.event;
|
||||
|
||||
private _onFillinConnectionInputs = new Emitter<IConnectionProfile>();
|
||||
public onFillinConnectionInputs: Event<IConnectionProfile> = this._onFillinConnectionInputs.event;
|
||||
|
||||
private _onResetConnection = new Emitter<void>();
|
||||
public onResetConnection: Event<void> = this._onResetConnection.event;
|
||||
|
||||
private _connecting = false;
|
||||
|
||||
constructor(
|
||||
private providerTypeOptions: string[],
|
||||
private selectedProviderType: string,
|
||||
private providerNameToDisplayNameMap: { [providerDisplayName: string]: string },
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IWorkbenchThemeService private _workbenchThemeService: IWorkbenchThemeService,
|
||||
@IPartService _partService: IPartService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IClipboardService clipboardService: IClipboardService
|
||||
) {
|
||||
super(localize('connection', 'Connection'), TelemetryKeys.Connection, _partService, telemetryService, clipboardService, _workbenchThemeService, contextKeyService, { hasSpinner: true, hasErrors: true });
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
let filteredProviderTypes = this.providerTypeOptions;
|
||||
|
||||
if (this._newConnectionParams && this._newConnectionParams.providers) {
|
||||
let validProviderNames = Object.keys(this.providerNameToDisplayNameMap).filter(x => this.includeProvider(x, this._newConnectionParams));
|
||||
if (validProviderNames && validProviderNames.length > 0) {
|
||||
filteredProviderTypes = filteredProviderTypes.filter(x => validProviderNames.find(v => this.providerNameToDisplayNameMap[v] === x) !== undefined);
|
||||
}
|
||||
}
|
||||
this._providerTypeSelectBox.setOptions(filteredProviderTypes);
|
||||
}
|
||||
|
||||
private includeProvider(providerName: string, params?: INewConnectionParams): Boolean {
|
||||
return params === undefined || params.providers === undefined || params.providers.find(x => x === providerName) !== undefined;
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
let connectionContainer = $('.connection-dialog');
|
||||
container.appendChild(connectionContainer.getHTMLElement());
|
||||
|
||||
this._bodyBuilder = new Builder(connectionContainer.getHTMLElement());
|
||||
const connectTypeLabel = localize('connectType', 'Connection type');
|
||||
this._providerTypeSelectBox = new SelectBox(this.providerTypeOptions, this.selectedProviderType, this._contextViewService, undefined, { ariaLabel: connectTypeLabel });
|
||||
// Recent connection tab
|
||||
let recentConnectionTab = $('.connection-recent-tab');
|
||||
recentConnectionTab.div({ class: 'connection-recent', id: 'recentConnection' }, (builder) => {
|
||||
this._recentConnectionBuilder = new Builder(builder.getHTMLElement());
|
||||
this._noRecentConnectionBuilder = new Builder(builder.getHTMLElement());
|
||||
this.createRecentConnections();
|
||||
this._recentConnectionBuilder.hide();
|
||||
});
|
||||
|
||||
// Saved connection tab
|
||||
let savedConnectionTab = $('.connection-saved-tab');
|
||||
savedConnectionTab.div({ class: 'connection-saved' }, (builder) => {
|
||||
this._savedConnectionBuilder = new Builder(builder.getHTMLElement());
|
||||
this._noSavedConnectionBuilder = new Builder(builder.getHTMLElement());
|
||||
this.createSavedConnections();
|
||||
this._savedConnectionBuilder.hide();
|
||||
});
|
||||
|
||||
this._panel = new TabbedPanel(connectionContainer.getHTMLElement());
|
||||
this._recentConnectionTabId = this._panel.pushTab({
|
||||
identifier: 'recent_connection',
|
||||
title: localize('recentConnectionTitle', 'Recent Connections'),
|
||||
view: {
|
||||
render: c => {
|
||||
recentConnectionTab.appendTo(c);
|
||||
},
|
||||
layout: () => { }
|
||||
}
|
||||
});
|
||||
|
||||
let savedConnectionTabId = this._panel.pushTab({
|
||||
identifier: 'saved_connection',
|
||||
title: localize('savedConnectionTitle', 'Saved Connections'),
|
||||
view: {
|
||||
layout: () => { },
|
||||
render: c => {
|
||||
savedConnectionTab.appendTo(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._panel.onTabChange(c => {
|
||||
if (c === savedConnectionTabId && this._savedConnectionTree.getContentHeight() === 0) {
|
||||
// Update saved connection tree
|
||||
TreeUpdateUtils.structuralTreeUpdate(this._savedConnectionTree, 'saved', this._connectionManagementService, this._providers);
|
||||
|
||||
if (this._savedConnectionTree.getContentHeight() > 0) {
|
||||
this._noSavedConnectionBuilder.hide();
|
||||
this._savedConnectionBuilder.show();
|
||||
} else {
|
||||
this._noSavedConnectionBuilder.show();
|
||||
this._savedConnectionBuilder.hide();
|
||||
}
|
||||
this._savedConnectionTree.layout(DOM.getTotalHeight(this._savedConnectionTree.getHTMLElement()));
|
||||
}
|
||||
});
|
||||
|
||||
this._bodyBuilder.div({ class: 'connection-details-title' }, (dividerContainer) => {
|
||||
this._connectionDetailTitle = dividerContainer;
|
||||
this._connectionDetailTitle.text(localize('connectionDetailsTitle', 'Connection Details'));
|
||||
});
|
||||
|
||||
this._bodyBuilder.div({ class: 'connection-type' }, (modelTableContent) => {
|
||||
modelTableContent.element('table', { class: 'connection-table-content' }, (tableContainer) => {
|
||||
DialogHelper.appendInputSelectBox(
|
||||
DialogHelper.appendRow(tableContainer, connectTypeLabel, 'connection-label', 'connection-input'), this._providerTypeSelectBox);
|
||||
});
|
||||
});
|
||||
|
||||
this.$connectionUIContainer = $('.connection-provider-info#connectionProviderInfo');
|
||||
this.$connectionUIContainer.appendTo(this._bodyBuilder);
|
||||
|
||||
let self = this;
|
||||
this._register(self._workbenchThemeService.onDidColorThemeChange(e => self.updateTheme(e)));
|
||||
self.updateTheme(self._workbenchThemeService.getColorTheme());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the connection flyout
|
||||
*/
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
let connectLabel = localize('connectionDialog.connect', 'Connect');
|
||||
let cancelLabel = localize('connectionDialog.cancel', 'Cancel');
|
||||
this._connectButton = this.addFooterButton(connectLabel, () => this.connect());
|
||||
this._connectButton.enabled = false;
|
||||
this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel());
|
||||
this.registerListeners();
|
||||
this.onProviderTypeSelected(this._providerTypeSelectBox.value);
|
||||
}
|
||||
|
||||
// Update theming that is specific to connection flyout body
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let borderColor = theme.getColor(contrastBorder);
|
||||
let border = borderColor ? borderColor.toString() : null;
|
||||
let backgroundColor = theme.getColor(SIDE_BAR_BACKGROUND);
|
||||
if (this._connectionDetailTitle) {
|
||||
this._connectionDetailTitle.style('border-width', border ? '1px 0px' : null);
|
||||
this._connectionDetailTitle.style('border-style', border ? 'solid none' : null);
|
||||
this._connectionDetailTitle.style('border-color', border);
|
||||
this._connectionDetailTitle.style('background-color', backgroundColor ? backgroundColor.toString() : null);
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(styler.attachSelectBoxStyler(this._providerTypeSelectBox, this._themeService));
|
||||
this._register(attachButtonStyler(this._connectButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
|
||||
this._register(this._providerTypeSelectBox.onDidSelect(selectedProviderType => {
|
||||
this.onProviderTypeSelected(selectedProviderType.selected);
|
||||
}));
|
||||
}
|
||||
|
||||
private onProviderTypeSelected(selectedProviderType: string) {
|
||||
// Show connection form based on server type
|
||||
this.$connectionUIContainer.empty();
|
||||
this._onShowUiComponent.fire({ selectedProviderType: selectedProviderType, container: this.$connectionUIContainer.getHTMLElement() });
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
private connect(element?: IConnectionProfile): void {
|
||||
if (this._connectButton.enabled) {
|
||||
this._connecting = true;
|
||||
this._connectButton.enabled = false;
|
||||
this._providerTypeSelectBox.disable();
|
||||
this.showSpinner();
|
||||
this._onConnect.fire(element);
|
||||
}
|
||||
}
|
||||
|
||||
/* Overwrite espace key behavior */
|
||||
protected onClose(e: StandardKeyboardEvent) {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept(e: StandardKeyboardEvent) {
|
||||
if (!e.target.classList.contains('monaco-tree')) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
let wasConnecting = this._connecting;
|
||||
this._onCancel.fire();
|
||||
if (!this._databaseDropdownExpanded && !wasConnecting) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.resetConnection();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private createRecentConnectionList(): void {
|
||||
this._recentConnectionBuilder.div({ class: 'connection-recent-content' }, (recentConnectionContainer) => {
|
||||
recentConnectionContainer.div({ class: 'recent-titles-container' }, (container) => {
|
||||
container.div({ class: 'connection-history-actions' }, (actionsContainer) => {
|
||||
this._actionbar = this._register(new ActionBar(actionsContainer.getHTMLElement(), { animated: false }));
|
||||
let clearAction = this._instantiationService.createInstance(ClearRecentConnectionsAction, ClearRecentConnectionsAction.ID, ClearRecentConnectionsAction.LABEL);
|
||||
clearAction.useConfirmationMessage = true;
|
||||
clearAction.onRecentConnectionsRemoved(() => this.open(false));
|
||||
this._actionbar.push(clearAction, { icon: true, label: true });
|
||||
});
|
||||
});
|
||||
recentConnectionContainer.div({ class: 'server-explorer-viewlet' }, (divContainer: Builder) => {
|
||||
divContainer.div({ class: 'explorer-servers' }, (treeContainer: Builder) => {
|
||||
let leftClick = (element: any, eventish: ICancelableEvent, origin: string) => {
|
||||
// element will be a server group if the tree is clicked rather than a item
|
||||
if (element instanceof ConnectionProfile) {
|
||||
this.onConnectionClick({ payload: { origin: origin, originalEvent: eventish } }, element);
|
||||
}
|
||||
};
|
||||
let actionProvider = this._instantiationService.createInstance(RecentConnectionActionsProvider);
|
||||
let controller = new RecentConnectionTreeController(leftClick, actionProvider, this._connectionManagementService, this._contextMenuService);
|
||||
actionProvider.onRecentConnectionRemoved(() => {
|
||||
this.open(this._connectionManagementService.getRecentConnections().length > 0);
|
||||
});
|
||||
controller.onRecentConnectionRemoved(() => {
|
||||
this.open(this._connectionManagementService.getRecentConnections().length > 0);
|
||||
});
|
||||
this._recentConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer.getHTMLElement(), this._instantiationService, controller);
|
||||
|
||||
// Theme styler
|
||||
this._register(styler.attachListStyler(this._recentConnectionTree, this._themeService));
|
||||
divContainer.append(this._recentConnectionTree.getHTMLElement());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createRecentConnections() {
|
||||
this.createRecentConnectionList();
|
||||
this._noRecentConnectionBuilder.div({ class: 'connection-recent-content' }, (noRecentConnectionContainer) => {
|
||||
let noRecentHistoryLabel = localize('noRecentConnections', 'No recent connection');
|
||||
noRecentConnectionContainer.div({ class: 'no-recent-connections' }, (noRecentTitle) => {
|
||||
noRecentTitle.text(noRecentHistoryLabel);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createSavedConnectionList(): void {
|
||||
this._savedConnectionBuilder.div({ class: 'connection-saved-content' }, (savedConnectioncontainer) => {
|
||||
savedConnectioncontainer.div({ class: 'server-explorer-viewlet' }, (divContainer: Builder) => {
|
||||
divContainer.div({ class: 'explorer-servers' }, (treeContainer: Builder) => {
|
||||
let leftClick = (element: any, eventish: ICancelableEvent, origin: string) => {
|
||||
// element will be a server group if the tree is clicked rather than a item
|
||||
if (element instanceof ConnectionProfile) {
|
||||
this.onConnectionClick({ payload: { origin: origin, originalEvent: eventish } }, element);
|
||||
}
|
||||
};
|
||||
|
||||
let controller = new SavedConnectionTreeController(leftClick);
|
||||
this._savedConnectionTree = TreeCreationUtils.createConnectionTree(treeContainer.getHTMLElement(), this._instantiationService, controller);
|
||||
|
||||
// Theme styler
|
||||
this._register(styler.attachListStyler(this._savedConnectionTree, this._themeService));
|
||||
divContainer.append(this._savedConnectionTree.getHTMLElement());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createSavedConnections() {
|
||||
this.createSavedConnectionList();
|
||||
this._noSavedConnectionBuilder.div({ class: 'connection-saved-content' }, (noSavedConnectionContainer) => {
|
||||
let noSavedConnectionLabel = localize('noSavedConnections', 'No saved connection');
|
||||
noSavedConnectionContainer.div({ class: 'no-saved-connections' }, (titleContainer) => {
|
||||
titleContainer.text(noSavedConnectionLabel);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private onConnectionClick(event: any, element: IConnectionProfile) {
|
||||
let isMouseOrigin = event.payload && (event.payload.origin === 'mouse');
|
||||
let isDoubleClick = isMouseOrigin && event.payload.originalEvent && event.payload.originalEvent.detail === 2;
|
||||
if (isDoubleClick) {
|
||||
this.connect(element);
|
||||
} else {
|
||||
if (element) {
|
||||
this._onFillinConnectionInputs.fire(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the flyout dialog
|
||||
* @param recentConnections Are there recent connections that should be shown
|
||||
*/
|
||||
public open(recentConnections: boolean) {
|
||||
this._panel.showTab(this._recentConnectionTabId);
|
||||
|
||||
this.show();
|
||||
if (recentConnections) {
|
||||
this._noRecentConnectionBuilder.hide();
|
||||
this._recentConnectionBuilder.show();
|
||||
} else {
|
||||
this._recentConnectionBuilder.hide();
|
||||
this._noRecentConnectionBuilder.show();
|
||||
}
|
||||
TreeUpdateUtils.structuralTreeUpdate(this._recentConnectionTree, 'recent', this._connectionManagementService, this._providers);
|
||||
|
||||
// reset saved connection tree
|
||||
this._savedConnectionTree.setInput([]);
|
||||
|
||||
// call layout with view height
|
||||
this.layout();
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
// Height is the overall height. Since we're laying out a specific component, always get its actual height
|
||||
this._recentConnectionTree.layout(DOM.getTotalHeight(this._recentConnectionTree.getHTMLElement()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state of the connect button
|
||||
* @param enabled The state to set the the button
|
||||
*/
|
||||
public set connectButtonState(enabled: boolean) {
|
||||
this._connectButton.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connect button state
|
||||
*/
|
||||
public get connectButtonState(): boolean {
|
||||
return this._connectButton.enabled;
|
||||
}
|
||||
|
||||
private initDialog(): void {
|
||||
super.setError('');
|
||||
this.hideSpinner();
|
||||
this._onInitDialog.fire();
|
||||
}
|
||||
|
||||
public resetConnection(): void {
|
||||
this.hideSpinner();
|
||||
this._connectButton.enabled = true;
|
||||
this._providerTypeSelectBox.enable();
|
||||
this._onResetConnection.fire();
|
||||
this._connecting = false;
|
||||
}
|
||||
|
||||
public get newConnectionParams(): INewConnectionParams {
|
||||
return this._newConnectionParams;
|
||||
}
|
||||
|
||||
public set newConnectionParams(params: INewConnectionParams) {
|
||||
this._newConnectionParams = params;
|
||||
this._providers = params && params.providers;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public updateProvider(displayName: string) {
|
||||
this._providerTypeSelectBox.selectWithOptionName(displayName);
|
||||
|
||||
this.onProviderTypeSelected(displayName);
|
||||
}
|
||||
|
||||
public set databaseDropdownExpanded(val: boolean) {
|
||||
this._databaseDropdownExpanded = val;
|
||||
}
|
||||
|
||||
public get databaseDropdownExpanded(): boolean {
|
||||
return this._databaseDropdownExpanded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./media/sqlConnection';
|
||||
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
||||
import { IConnectionComponentCallbacks } from 'sql/workbench/services/connection/browser/connectionDialogService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import * as Constants from 'sql/platform/connection/common/constants';
|
||||
import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
|
||||
import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import * as styler from 'sql/platform/theme/common/styler';
|
||||
import { IAccountManagementService } from 'sql/platform/accountManagement/common/interfaces';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
import * as lifecycle from 'vs/base/common/lifecycle';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { Builder, $ } from 'vs/base/browser/builder';
|
||||
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { endsWith, startsWith } from 'vs/base/common/strings';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export class ConnectionWidget {
|
||||
private _builder: Builder;
|
||||
private _serverGroupSelectBox: SelectBox;
|
||||
private _previousGroupOption: string;
|
||||
private _serverGroupOptions: IConnectionProfileGroup[];
|
||||
private _connectionNameInputBox: InputBox;
|
||||
private _serverNameInputBox: InputBox;
|
||||
private _databaseNameInputBox: Dropdown;
|
||||
private _userNameInputBox: InputBox;
|
||||
private _passwordInputBox: InputBox;
|
||||
private _password: string;
|
||||
private _rememberPasswordCheckBox: Checkbox;
|
||||
private _azureAccountDropdown: SelectBox;
|
||||
private _azureTenantDropdown: SelectBox;
|
||||
private _refreshCredentialsLinkBuilder: Builder;
|
||||
private _addAzureAccountMessage: string = localize('connectionWidget.AddAzureAccount', 'Add an account...');
|
||||
private readonly _azureProviderId = 'azurePublicCloud';
|
||||
private _azureTenantId: string;
|
||||
private _azureAccountList: sqlops.Account[];
|
||||
private _advancedButton: Button;
|
||||
private _callbacks: IConnectionComponentCallbacks;
|
||||
private _authTypeSelectBox: SelectBox;
|
||||
private _toDispose: lifecycle.IDisposable[];
|
||||
private _optionsMaps: { [optionType: number]: sqlops.ConnectionOption };
|
||||
private _tableContainer: Builder;
|
||||
private _focusedBeforeHandleOnConnection: HTMLElement;
|
||||
private _providerName: string;
|
||||
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
|
||||
[Constants.mssqlProviderName]: [AuthenticationType.SqlLogin, AuthenticationType.Integrated, AuthenticationType.AzureMFA]
|
||||
};
|
||||
private _saveProfile: boolean;
|
||||
private _databaseDropdownExpanded: boolean = false;
|
||||
private _defaultDatabaseName: string = localize('defaultDatabaseOption', '<Default>');
|
||||
private _loadingDatabaseName: string = localize('loadingDatabaseOption', 'Loading...');
|
||||
private _serverGroupDisplayString: string = localize('serverGroup', 'Server group');
|
||||
public DefaultServerGroup: IConnectionProfileGroup = {
|
||||
id: '',
|
||||
name: localize('defaultServerGroup', '<Default>'),
|
||||
parentId: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
private _addNewServerGroup = {
|
||||
id: '',
|
||||
name: localize('addNewServerGroup', 'Add new group...'),
|
||||
parentId: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
public NoneServerGroup: IConnectionProfileGroup = {
|
||||
id: '',
|
||||
name: localize('noneServerGroup', '<Do not save>'),
|
||||
parentId: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
constructor(options: sqlops.ConnectionOption[],
|
||||
callbacks: IConnectionComponentCallbacks,
|
||||
providerName: string,
|
||||
@IThemeService private _themeService: IThemeService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||
) {
|
||||
this._callbacks = callbacks;
|
||||
this._toDispose = [];
|
||||
this._optionsMaps = {};
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var option = options[i];
|
||||
this._optionsMaps[option.specialValueType] = option;
|
||||
}
|
||||
|
||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
if (authTypeOption) {
|
||||
if (OS === OperatingSystem.Windows) {
|
||||
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated);
|
||||
} else {
|
||||
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin);
|
||||
}
|
||||
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
|
||||
}
|
||||
this._providerName = providerName;
|
||||
}
|
||||
|
||||
public createConnectionWidget(container: HTMLElement): void {
|
||||
this._serverGroupOptions = [this.DefaultServerGroup];
|
||||
this._serverGroupSelectBox = new SelectBox(this._serverGroupOptions.map(g => g.name), this.DefaultServerGroup.name, this._contextViewService, undefined, { ariaLabel: this._serverGroupDisplayString });
|
||||
this._previousGroupOption = this._serverGroupSelectBox.value;
|
||||
this._builder = $().div({ class: 'connection-table' }, (modelTableContent) => {
|
||||
modelTableContent.element('table', { class: 'connection-table-content' }, (tableContainer) => {
|
||||
this._tableContainer = tableContainer;
|
||||
});
|
||||
});
|
||||
this.fillInConnectionForm();
|
||||
this.registerListeners();
|
||||
if (this._authTypeSelectBox) {
|
||||
this.onAuthTypeSelected(this._authTypeSelectBox.value);
|
||||
}
|
||||
|
||||
DOM.addDisposableListener(container, 'paste', e => {
|
||||
this._handleClipboard();
|
||||
});
|
||||
|
||||
DOM.append(container, this._builder.getHTMLElement());
|
||||
}
|
||||
|
||||
private _handleClipboard(): void {
|
||||
if (this._configurationService.getValue<boolean>('connection.parseClipboardForConnectionString')) {
|
||||
let paste = this._clipboardService.readText();
|
||||
this._connectionManagementService.buildConnectionInfo(paste, this._providerName).then(e => {
|
||||
if (e) {
|
||||
let profile = new ConnectionProfile(this._capabilitiesService, this._providerName);
|
||||
profile.options = e.options;
|
||||
if (profile.serverName) {
|
||||
this.initDialog(profile);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private fillInConnectionForm(): void {
|
||||
// Server name
|
||||
let serverNameOption = this._optionsMaps[ConnectionOptionSpecialType.serverName];
|
||||
let serverNameBuilder = DialogHelper.appendRow(this._tableContainer, serverNameOption.displayName, 'connection-label', 'connection-input');
|
||||
this._serverNameInputBox = new InputBox(serverNameBuilder.getHTMLElement(), this._contextViewService, {
|
||||
validationOptions: {
|
||||
validation: (value: string) => {
|
||||
if (!value) {
|
||||
return ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', '{0} is required.', serverNameOption.displayName) });
|
||||
} else if (startsWith(value, ' ') || endsWith(value, ' ')) {
|
||||
return ({ type: MessageType.WARNING, content: localize('connectionWidget.fieldWillBeTrimmed', '{0} will be trimmed.', serverNameOption.displayName) });
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
ariaLabel: serverNameOption.displayName
|
||||
});
|
||||
|
||||
// Authentication type
|
||||
if (this._optionsMaps[ConnectionOptionSpecialType.authType]) {
|
||||
let authTypeBuilder = DialogHelper.appendRow(this._tableContainer, this._optionsMaps[ConnectionOptionSpecialType.authType].displayName, 'connection-label', 'connection-input');
|
||||
DialogHelper.appendInputSelectBox(authTypeBuilder, this._authTypeSelectBox);
|
||||
}
|
||||
|
||||
// Username
|
||||
let self = this;
|
||||
let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
|
||||
let userNameBuilder = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input', 'username-password-row');
|
||||
this._userNameInputBox = new InputBox(userNameBuilder.getHTMLElement(), this._contextViewService, {
|
||||
validationOptions: {
|
||||
validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', '{0} is required.', userNameOption.displayName) }) : null
|
||||
},
|
||||
ariaLabel: userNameOption.displayName
|
||||
});
|
||||
// Password
|
||||
let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password];
|
||||
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input', 'username-password-row');
|
||||
this._passwordInputBox = new InputBox(passwordBuilder.getHTMLElement(), this._contextViewService, { ariaLabel: passwordOption.displayName });
|
||||
this._passwordInputBox.inputElement.type = 'password';
|
||||
this._password = '';
|
||||
|
||||
// Remember password
|
||||
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
|
||||
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', 'username-password-row', false);
|
||||
|
||||
// Azure account picker
|
||||
let accountLabel = localize('connection.azureAccountDropdownLabel', 'Account');
|
||||
let accountDropdownBuilder = DialogHelper.appendRow(this._tableContainer, accountLabel, 'connection-label', 'connection-input', 'azure-account-row');
|
||||
this._azureAccountDropdown = new SelectBox([], undefined, this._contextViewService, accountDropdownBuilder.getContainer(), { ariaLabel: accountLabel });
|
||||
DialogHelper.appendInputSelectBox(accountDropdownBuilder, this._azureAccountDropdown);
|
||||
let refreshCredentialsBuilder = DialogHelper.appendRow(this._tableContainer, '', 'connection-label', 'connection-input', 'azure-account-row refresh-credentials-link');
|
||||
this._refreshCredentialsLinkBuilder = refreshCredentialsBuilder.a({ href: '#' }).text(localize('connectionWidget.refreshAzureCredentials', 'Refresh account credentials'));
|
||||
|
||||
// Azure tenant picker
|
||||
let tenantLabel = localize('connection.azureTenantDropdownLabel', 'Azure AD tenant');
|
||||
let tenantDropdownBuilder = DialogHelper.appendRow(this._tableContainer, tenantLabel, 'connection-label', 'connection-input', 'azure-account-row azure-tenant-row');
|
||||
this._azureTenantDropdown = new SelectBox([], undefined, this._contextViewService, tenantDropdownBuilder.getContainer(), { ariaLabel: tenantLabel });
|
||||
DialogHelper.appendInputSelectBox(tenantDropdownBuilder, this._azureTenantDropdown);
|
||||
|
||||
// Database
|
||||
let databaseOption = this._optionsMaps[ConnectionOptionSpecialType.databaseName];
|
||||
let databaseNameBuilder = DialogHelper.appendRow(this._tableContainer, databaseOption.displayName, 'connection-label', 'connection-input');
|
||||
|
||||
this._databaseNameInputBox = new Dropdown(databaseNameBuilder.getHTMLElement(), this._contextViewService, this._themeService, {
|
||||
values: [this._defaultDatabaseName, this._loadingDatabaseName],
|
||||
strictSelection: false,
|
||||
placeholder: this._defaultDatabaseName,
|
||||
maxHeight: 125,
|
||||
ariaLabel: databaseOption.displayName,
|
||||
actionLabel: localize('connectionWidget.toggleDatabaseNameDropdown', 'Select Database Toggle Dropdown')
|
||||
});
|
||||
|
||||
// Server group
|
||||
let serverGroupBuilder = DialogHelper.appendRow(this._tableContainer, this._serverGroupDisplayString, 'connection-label', 'connection-input');
|
||||
DialogHelper.appendInputSelectBox(serverGroupBuilder, this._serverGroupSelectBox);
|
||||
|
||||
// Connection name
|
||||
let connectionNameOption = this._optionsMaps[ConnectionOptionSpecialType.connectionName];
|
||||
let connectionNameBuilder = DialogHelper.appendRow(this._tableContainer, connectionNameOption.displayName, 'connection-label', 'connection-input');
|
||||
this._connectionNameInputBox = new InputBox(connectionNameBuilder.getHTMLElement(), this._contextViewService, { ariaLabel: connectionNameOption.displayName });
|
||||
|
||||
let AdvancedLabel = localize('advanced', 'Advanced...');
|
||||
this._advancedButton = this.createAdvancedButton(this._tableContainer, AdvancedLabel);
|
||||
}
|
||||
|
||||
private validateUsername(value: string, isOptionRequired: boolean): boolean {
|
||||
let currentAuthType = this._authTypeSelectBox ? this.getMatchingAuthType(this._authTypeSelectBox.value) : undefined;
|
||||
if (!currentAuthType || currentAuthType === AuthenticationType.SqlLogin) {
|
||||
if (!value && isOptionRequired) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private createAdvancedButton(container: Builder, title: string): Button {
|
||||
let button;
|
||||
container.element('tr', {}, (rowContainer) => {
|
||||
rowContainer.element('td');
|
||||
rowContainer.element('td', { align: 'right' }, (cellContainer) => {
|
||||
cellContainer.div({ class: 'advanced-button' }, (divContainer) => {
|
||||
button = new Button(divContainer);
|
||||
button.label = title;
|
||||
button.onDidClick(() => {
|
||||
//open advanced page
|
||||
this._callbacks.onAdvancedProperties();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private appendCheckbox(container: Builder, label: string, checkboxClass: string, cellContainerClass: string, rowContainerClass: string, isChecked: boolean): Checkbox {
|
||||
let checkbox: Checkbox;
|
||||
container.element('tr', { class: rowContainerClass }, (rowContainer) => {
|
||||
rowContainer.element('td');
|
||||
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
|
||||
checkbox = new Checkbox(inputCellContainer.getHTMLElement(), { label, checked: isChecked, ariaLabel: label });
|
||||
});
|
||||
});
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._toDispose.push(styler.attachInputBoxStyler(this._serverNameInputBox, this._themeService));
|
||||
this._toDispose.push(styler.attachEditableDropdownStyler(this._databaseNameInputBox, this._themeService));
|
||||
this._toDispose.push(styler.attachInputBoxStyler(this._connectionNameInputBox, this._themeService));
|
||||
this._toDispose.push(styler.attachInputBoxStyler(this._userNameInputBox, this._themeService));
|
||||
this._toDispose.push(styler.attachInputBoxStyler(this._passwordInputBox, this._themeService));
|
||||
this._toDispose.push(styler.attachSelectBoxStyler(this._serverGroupSelectBox, this._themeService));
|
||||
this._toDispose.push(styler.attachButtonStyler(this._advancedButton, this._themeService));
|
||||
this._toDispose.push(styler.attachCheckboxStyler(this._rememberPasswordCheckBox, this._themeService));
|
||||
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
|
||||
|
||||
if (this._authTypeSelectBox) {
|
||||
// Theme styler
|
||||
this._toDispose.push(styler.attachSelectBoxStyler(this._authTypeSelectBox, this._themeService));
|
||||
this._toDispose.push(this._authTypeSelectBox.onDidSelect(selectedAuthType => {
|
||||
this.onAuthTypeSelected(selectedAuthType.selected);
|
||||
this.setConnectButton();
|
||||
}));
|
||||
}
|
||||
|
||||
if (this._azureAccountDropdown) {
|
||||
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
|
||||
this._toDispose.push(this._azureAccountDropdown.onDidSelect(() => {
|
||||
this.onAzureAccountSelected();
|
||||
}));
|
||||
}
|
||||
|
||||
if (this._azureTenantDropdown) {
|
||||
this._toDispose.push(styler.attachSelectBoxStyler(this._azureTenantDropdown, this._themeService));
|
||||
this._toDispose.push(this._azureTenantDropdown.onDidSelect((selectInfo) => {
|
||||
this.onAzureTenantSelected(selectInfo.index);
|
||||
}));
|
||||
}
|
||||
|
||||
if (this._refreshCredentialsLinkBuilder) {
|
||||
this._toDispose.push(this._refreshCredentialsLinkBuilder.on(DOM.EventType.CLICK, async () => {
|
||||
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (account) {
|
||||
await this._accountManagementService.refreshAccount(account);
|
||||
this.fillInAzureAccountOptions();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._toDispose.push(this._serverGroupSelectBox.onDidSelect(selectedGroup => {
|
||||
this.onGroupSelected(selectedGroup.selected);
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._serverNameInputBox.onDidChange(serverName => {
|
||||
this.serverNameChanged(serverName);
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._userNameInputBox.onDidChange(userName => {
|
||||
this.setConnectButton();
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._passwordInputBox.onDidChange(passwordInput => {
|
||||
this._password = passwordInput;
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._databaseNameInputBox.onFocus(() => {
|
||||
this._databaseDropdownExpanded = true;
|
||||
if (this.serverName) {
|
||||
this._databaseNameInputBox.values = [this._loadingDatabaseName];
|
||||
this._callbacks.onFetchDatabases(this.serverName, this.authenticationType, this.userName, this._password).then(databases => {
|
||||
if (databases) {
|
||||
this._databaseNameInputBox.values = databases.sort((a, b) => a.localeCompare(b));
|
||||
} else {
|
||||
this._databaseNameInputBox.values = [this._defaultDatabaseName];
|
||||
}
|
||||
}).catch(() => {
|
||||
this._databaseNameInputBox.values = [this._defaultDatabaseName];
|
||||
});
|
||||
} else {
|
||||
this._databaseNameInputBox.values = [this._defaultDatabaseName];
|
||||
}
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._databaseNameInputBox.onValueChange(s => {
|
||||
if (s === this._defaultDatabaseName || s === this._loadingDatabaseName) {
|
||||
this._databaseNameInputBox.value = '';
|
||||
} else {
|
||||
this._databaseNameInputBox.value = s;
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
private onGroupSelected(selectedGroup: string) {
|
||||
if (selectedGroup === this._addNewServerGroup.name) {
|
||||
// Select previous non-AddGroup option in case AddServerGroup dialog is cancelled
|
||||
this._serverGroupSelectBox.selectWithOptionName(this._previousGroupOption);
|
||||
this._callbacks.onCreateNewServerGroup();
|
||||
} else {
|
||||
this._previousGroupOption = selectedGroup;
|
||||
}
|
||||
}
|
||||
|
||||
private setConnectButton(): void {
|
||||
let showUsernameAndPassword: boolean = true;
|
||||
if (this.authType) {
|
||||
showUsernameAndPassword = this.authType === AuthenticationType.SqlLogin;
|
||||
}
|
||||
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
|
||||
this._callbacks.onSetConnectButton(!!this.serverName);
|
||||
}
|
||||
|
||||
private onAuthTypeSelected(selectedAuthType: string) {
|
||||
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
|
||||
if (currentAuthType !== AuthenticationType.SqlLogin) {
|
||||
this._userNameInputBox.disable();
|
||||
this._passwordInputBox.disable();
|
||||
this._userNameInputBox.hideMessage();
|
||||
this._passwordInputBox.hideMessage();
|
||||
this._userNameInputBox.value = '';
|
||||
this._passwordInputBox.value = '';
|
||||
this._password = '';
|
||||
|
||||
this._rememberPasswordCheckBox.checked = false;
|
||||
this._rememberPasswordCheckBox.enabled = false;
|
||||
} else {
|
||||
this._userNameInputBox.enable();
|
||||
this._passwordInputBox.enable();
|
||||
this._rememberPasswordCheckBox.enabled = true;
|
||||
}
|
||||
|
||||
if (currentAuthType === AuthenticationType.AzureMFA) {
|
||||
this.fillInAzureAccountOptions();
|
||||
this._azureAccountDropdown.enable();
|
||||
let tableContainer = this._tableContainer.getContainer();
|
||||
tableContainer.classList.add('hide-username-password');
|
||||
tableContainer.classList.remove('hide-azure-accounts');
|
||||
} else {
|
||||
this._azureAccountDropdown.disable();
|
||||
let tableContainer = this._tableContainer.getContainer();
|
||||
tableContainer.classList.remove('hide-username-password');
|
||||
tableContainer.classList.add('hide-azure-accounts');
|
||||
this._azureAccountDropdown.hideMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private async fillInAzureAccountOptions(): Promise<void> {
|
||||
let oldSelection = this._azureAccountDropdown.value;
|
||||
this._azureAccountList = await this._accountManagementService.getAccountsForProvider(this._azureProviderId);
|
||||
let accountDropdownOptions = this._azureAccountList.map(account => account.key.accountId);
|
||||
if (accountDropdownOptions.length === 0) {
|
||||
// If there are no accounts add a blank option so that add account isn't automatically selected
|
||||
accountDropdownOptions.unshift('');
|
||||
}
|
||||
accountDropdownOptions.push(this._addAzureAccountMessage);
|
||||
this._azureAccountDropdown.setOptions(accountDropdownOptions);
|
||||
this._azureAccountDropdown.selectWithOptionName(oldSelection);
|
||||
await this.onAzureAccountSelected();
|
||||
}
|
||||
|
||||
private async updateRefreshCredentialsLink(): Promise<void> {
|
||||
let chosenAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (chosenAccount && chosenAccount.isStale) {
|
||||
this._tableContainer.getContainer().classList.remove('hide-refresh-link');
|
||||
} else {
|
||||
this._tableContainer.getContainer().classList.add('hide-refresh-link');
|
||||
}
|
||||
}
|
||||
|
||||
private async onAzureAccountSelected(): Promise<void> {
|
||||
// Reset the dropdown's validation message if the old selection was not valid but the new one is
|
||||
this.validateAzureAccountSelection(false);
|
||||
|
||||
// Open the add account dialog if needed, then select the added account
|
||||
if (this._azureAccountDropdown.value === this._addAzureAccountMessage) {
|
||||
let oldAccountIds = this._azureAccountList.map(account => account.key.accountId);
|
||||
await this._accountManagementService.addAccount(this._azureProviderId);
|
||||
|
||||
// Refresh the dropdown's list to include the added account
|
||||
await this.fillInAzureAccountOptions();
|
||||
|
||||
// If a new account was added find it and select it, otherwise select the first account
|
||||
let newAccount = this._azureAccountList.find(option => !oldAccountIds.some(oldId => oldId === option.key.accountId));
|
||||
if (newAccount) {
|
||||
this._azureAccountDropdown.selectWithOptionName(newAccount.key.accountId);
|
||||
} else {
|
||||
this._azureAccountDropdown.select(0);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateRefreshCredentialsLink();
|
||||
|
||||
// Display the tenant select box if needed
|
||||
const hideTenantsClassName = 'hide-azure-tenants';
|
||||
let selectedAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (selectedAccount && selectedAccount.properties.tenants && selectedAccount.properties.tenants.length > 1) {
|
||||
// There are multiple tenants available so let the user select one
|
||||
let options = selectedAccount.properties.tenants.map(tenant => tenant.displayName);
|
||||
this._azureTenantDropdown.setOptions(options);
|
||||
this._tableContainer.getContainer().classList.remove(hideTenantsClassName);
|
||||
this.onAzureTenantSelected(0);
|
||||
} else {
|
||||
if (selectedAccount && selectedAccount.properties.tenants && selectedAccount.properties.tenants.length === 1) {
|
||||
this._azureTenantId = selectedAccount.properties.tenants[0].id;
|
||||
} else {
|
||||
this._azureTenantId = undefined;
|
||||
}
|
||||
this._tableContainer.getContainer().classList.add(hideTenantsClassName);
|
||||
}
|
||||
}
|
||||
|
||||
private onAzureTenantSelected(tenantIndex: number): void {
|
||||
this._azureTenantId = undefined;
|
||||
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (account && account.properties.tenants) {
|
||||
let tenant = account.properties.tenants[tenantIndex];
|
||||
if (tenant) {
|
||||
this._azureTenantId = tenant.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private serverNameChanged(serverName: string) {
|
||||
this.setConnectButton();
|
||||
if (serverName.toLocaleLowerCase().includes('database.windows.net')) {
|
||||
this._callbacks.onSetAzureTimeOut();
|
||||
}
|
||||
}
|
||||
|
||||
public focusOnAdvancedButton() {
|
||||
this._advancedButton.focus();
|
||||
}
|
||||
|
||||
public focusOnServerGroup() {
|
||||
this._serverGroupSelectBox.focus();
|
||||
}
|
||||
|
||||
public updateServerGroup(connectionGroups: IConnectionProfileGroup[], groupName?: string) {
|
||||
this._serverGroupOptions = connectionGroups;
|
||||
this._serverGroupOptions.push(this._addNewServerGroup);
|
||||
this._serverGroupSelectBox.setOptions(this._serverGroupOptions.map(g => g.name));
|
||||
if (groupName) {
|
||||
this._serverGroupSelectBox.selectWithOptionName(groupName);
|
||||
this._previousGroupOption = this._serverGroupSelectBox.value;
|
||||
}
|
||||
}
|
||||
|
||||
public initDialog(connectionInfo: IConnectionProfile): void {
|
||||
this.fillInConnectionInputs(connectionInfo);
|
||||
}
|
||||
|
||||
public focusOnOpen(): void {
|
||||
this._handleClipboard();
|
||||
this._serverNameInputBox.focus();
|
||||
this.focusPasswordIfNeeded();
|
||||
this.clearValidationMessages();
|
||||
}
|
||||
|
||||
private clearValidationMessages(): void {
|
||||
this._serverNameInputBox.hideMessage();
|
||||
this._userNameInputBox.hideMessage();
|
||||
this._azureAccountDropdown.hideMessage();
|
||||
}
|
||||
|
||||
private getModelValue(value: string): string {
|
||||
return value ? value : '';
|
||||
}
|
||||
|
||||
public fillInConnectionInputs(connectionInfo: IConnectionProfile) {
|
||||
if (connectionInfo) {
|
||||
this._serverNameInputBox.value = this.getModelValue(connectionInfo.serverName);
|
||||
this._databaseNameInputBox.value = this.getModelValue(connectionInfo.databaseName);
|
||||
this._connectionNameInputBox.value = this.getModelValue(connectionInfo.connectionName);
|
||||
this._userNameInputBox.value = this.getModelValue(connectionInfo.userName);
|
||||
this._passwordInputBox.value = connectionInfo.password ? Constants.passwordChars : '';
|
||||
this._password = this.getModelValue(connectionInfo.password);
|
||||
this._saveProfile = connectionInfo.saveProfile;
|
||||
this._azureTenantId = connectionInfo.azureTenantId;
|
||||
let groupName: string;
|
||||
if (this._saveProfile) {
|
||||
if (!connectionInfo.groupFullName) {
|
||||
groupName = this.DefaultServerGroup.name;
|
||||
} else {
|
||||
groupName = connectionInfo.groupFullName.replace('root/', '');
|
||||
}
|
||||
} else {
|
||||
groupName = this.NoneServerGroup.name;
|
||||
}
|
||||
this._serverGroupSelectBox.selectWithOptionName(groupName);
|
||||
this._previousGroupOption = this._serverGroupSelectBox.value;
|
||||
|
||||
// To handle the empty password case
|
||||
if (this.getModelValue(connectionInfo.password) === '') {
|
||||
this._rememberPasswordCheckBox.checked = false;
|
||||
} else {
|
||||
this._rememberPasswordCheckBox.checked = connectionInfo.savePassword;
|
||||
}
|
||||
|
||||
if (connectionInfo.authenticationType !== null && connectionInfo.authenticationType !== undefined) {
|
||||
var authTypeDisplayName = this.getAuthTypeDisplayName(connectionInfo.authenticationType);
|
||||
this._authTypeSelectBox.selectWithOptionName(authTypeDisplayName);
|
||||
}
|
||||
|
||||
if (this._authTypeSelectBox) {
|
||||
this.onAuthTypeSelected(this._authTypeSelectBox.value);
|
||||
} else {
|
||||
let tableContainerElement = this._tableContainer.getContainer();
|
||||
tableContainerElement.classList.remove('hide-username-password');
|
||||
tableContainerElement.classList.add('hide-azure-accounts');
|
||||
}
|
||||
|
||||
if (this.authType === AuthenticationType.AzureMFA) {
|
||||
this.fillInAzureAccountOptions().then(async () => {
|
||||
this._azureAccountDropdown.selectWithOptionName(this.getModelValue(connectionInfo.userName));
|
||||
await this.onAzureAccountSelected();
|
||||
let tenantId = connectionInfo.azureTenantId;
|
||||
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||
if (account && account.properties.tenants.length > 1) {
|
||||
let tenant = account.properties.tenants.find(tenant => tenant.id === tenantId);
|
||||
if (tenant) {
|
||||
this._azureTenantDropdown.selectWithOptionName(tenant.displayName);
|
||||
}
|
||||
this.onAzureTenantSelected(this._azureTenantDropdown.values.indexOf(this._azureTenantDropdown.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Disable connect button if -
|
||||
// 1. Authentication type is SQL Login and no username is provided
|
||||
// 2. No server name is provided
|
||||
this.setConnectButton();
|
||||
this.focusPasswordIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
private getAuthTypeDisplayName(authTypeName: string) {
|
||||
var displayName: string;
|
||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
|
||||
if (authTypeOption) {
|
||||
authTypeOption.categoryValues.forEach(c => {
|
||||
if (c.name === authTypeName) {
|
||||
displayName = c.displayName;
|
||||
}
|
||||
});
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
private getAuthTypeName(authTypeDisplayName: string) {
|
||||
var authTypeName: string;
|
||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||
authTypeOption.categoryValues.forEach(c => {
|
||||
if (c.displayName === authTypeDisplayName) {
|
||||
authTypeName = c.name;
|
||||
}
|
||||
});
|
||||
return authTypeName;
|
||||
}
|
||||
|
||||
public handleOnConnecting(): void {
|
||||
this._focusedBeforeHandleOnConnection = <HTMLElement>document.activeElement;
|
||||
this._advancedButton.enabled = false;
|
||||
|
||||
this._serverGroupSelectBox.disable();
|
||||
this._serverNameInputBox.disable();
|
||||
this._databaseNameInputBox.enabled = false;
|
||||
this._userNameInputBox.disable();
|
||||
this._passwordInputBox.disable();
|
||||
this._connectionNameInputBox.disable();
|
||||
this._rememberPasswordCheckBox.enabled = false;
|
||||
if (this._authTypeSelectBox) {
|
||||
this._authTypeSelectBox.disable();
|
||||
}
|
||||
}
|
||||
|
||||
public handleResetConnection(): void {
|
||||
this._advancedButton.enabled = true;
|
||||
|
||||
this._serverGroupSelectBox.enable();
|
||||
this._serverNameInputBox.enable();
|
||||
this._connectionNameInputBox.enable();
|
||||
this._databaseNameInputBox.enabled = true;
|
||||
let currentAuthType: AuthenticationType = undefined;
|
||||
if (this._authTypeSelectBox) {
|
||||
this._authTypeSelectBox.enable();
|
||||
currentAuthType = this.getMatchingAuthType(this._authTypeSelectBox.value);
|
||||
}
|
||||
|
||||
if (!currentAuthType || currentAuthType === AuthenticationType.SqlLogin) {
|
||||
this._userNameInputBox.enable();
|
||||
this._passwordInputBox.enable();
|
||||
this._rememberPasswordCheckBox.enabled = true;
|
||||
}
|
||||
|
||||
if (this._focusedBeforeHandleOnConnection) {
|
||||
this._focusedBeforeHandleOnConnection.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public get connectionName(): string {
|
||||
return this._connectionNameInputBox.value;
|
||||
}
|
||||
|
||||
public get serverName(): string {
|
||||
return this._serverNameInputBox.value;
|
||||
}
|
||||
|
||||
public get databaseName(): string {
|
||||
return this._databaseNameInputBox.value;
|
||||
}
|
||||
|
||||
public get userName(): string {
|
||||
return this.authenticationType === AuthenticationType.AzureMFA ? this._azureAccountDropdown.value : this._userNameInputBox.value;
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
return this._password;
|
||||
}
|
||||
|
||||
public get authenticationType(): string {
|
||||
return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined;
|
||||
}
|
||||
|
||||
private validateAzureAccountSelection(showMessage: boolean = true): boolean {
|
||||
if (this.authType !== AuthenticationType.AzureMFA) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let selected = this._azureAccountDropdown.value;
|
||||
if (selected === '' || selected === this._addAzureAccountMessage) {
|
||||
if (showMessage) {
|
||||
this._azureAccountDropdown.showMessage({
|
||||
content: localize('connectionWidget.invalidAzureAccount', 'You must select an account'),
|
||||
type: MessageType.ERROR
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
this._azureAccountDropdown.hideMessage();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private validateInputs(): boolean {
|
||||
let isFocused = false;
|
||||
let validateServerName = this._serverNameInputBox.validate();
|
||||
if (!validateServerName) {
|
||||
this._serverNameInputBox.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
let validateUserName = this._userNameInputBox.validate();
|
||||
if (!validateUserName && !isFocused) {
|
||||
this._userNameInputBox.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
let validatePassword = this._passwordInputBox.validate();
|
||||
if (!validatePassword && !isFocused) {
|
||||
this._passwordInputBox.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
let validateAzureAccount = this.validateAzureAccountSelection();
|
||||
if (!validateAzureAccount && !isFocused) {
|
||||
this._azureAccountDropdown.focus();
|
||||
isFocused = true;
|
||||
}
|
||||
return validateServerName && validateUserName && validatePassword && validateAzureAccount;
|
||||
}
|
||||
|
||||
public connect(model: IConnectionProfile): boolean {
|
||||
let validInputs = this.validateInputs();
|
||||
if (validInputs) {
|
||||
model.connectionName = this.connectionName;
|
||||
model.serverName = this.serverName;
|
||||
model.databaseName = this.databaseName;
|
||||
model.userName = this.userName;
|
||||
model.password = this.password;
|
||||
model.authenticationType = this.authenticationType;
|
||||
model.savePassword = this._rememberPasswordCheckBox.checked;
|
||||
if (this._serverGroupSelectBox.value === this.DefaultServerGroup.name) {
|
||||
model.groupFullName = '';
|
||||
model.saveProfile = true;
|
||||
model.groupId = this.findGroupId(model.groupFullName);
|
||||
} else if (this._serverGroupSelectBox.value === this.NoneServerGroup.name) {
|
||||
model.groupFullName = '';
|
||||
model.saveProfile = false;
|
||||
} else if (this._serverGroupSelectBox.value !== this._addNewServerGroup.name) {
|
||||
model.groupFullName = this._serverGroupSelectBox.value;
|
||||
model.saveProfile = true;
|
||||
model.groupId = this.findGroupId(model.groupFullName);
|
||||
}
|
||||
if (this.authType === AuthenticationType.AzureMFA) {
|
||||
model.azureTenantId = this._azureTenantId;
|
||||
}
|
||||
}
|
||||
return validInputs;
|
||||
}
|
||||
|
||||
private findGroupId(groupFullName: string): string {
|
||||
let group: IConnectionProfileGroup;
|
||||
if (ConnectionProfileGroup.isRoot(groupFullName)) {
|
||||
group = this._serverGroupOptions.find(g => ConnectionProfileGroup.isRoot(g.name));
|
||||
if (group === undefined) {
|
||||
group = this._serverGroupOptions.find(g => g.name === this.DefaultServerGroup.name);
|
||||
}
|
||||
} else {
|
||||
group = this._serverGroupOptions.find(g => g.name === groupFullName);
|
||||
}
|
||||
return group ? group.id : undefined;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toDispose = lifecycle.dispose(this._toDispose);
|
||||
}
|
||||
|
||||
private getMatchingAuthType(displayName: string): AuthenticationType {
|
||||
const authType = this._authTypeMap[this._providerName];
|
||||
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType) === displayName) : undefined;
|
||||
}
|
||||
|
||||
public closeDatabaseDropdown(): void {
|
||||
this._databaseNameInputBox.blur();
|
||||
}
|
||||
|
||||
public get databaseDropdownExpanded(): boolean {
|
||||
return this._databaseDropdownExpanded;
|
||||
}
|
||||
|
||||
public set databaseDropdownExpanded(val: boolean) {
|
||||
this._databaseDropdownExpanded = val;
|
||||
}
|
||||
|
||||
private get authType(): AuthenticationType {
|
||||
let authDisplayName: string = this.getAuthTypeDisplayName(this.authenticationType);
|
||||
return this.getMatchingAuthType(authDisplayName);
|
||||
}
|
||||
|
||||
private focusPasswordIfNeeded(): void {
|
||||
if (this.authType && this.authType === AuthenticationType.SqlLogin && this.userName && !this.password) {
|
||||
this._passwordInputBox.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthenticationType {
|
||||
SqlLogin = 'SqlLogin',
|
||||
Integrated = 'Integrated',
|
||||
AzureMFA = 'AzureMFA'
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#1E1E1E" d="M4.222 0h-2.222v.479c-.526.648-.557 1.57-.043 2.269l.043.059v3.203l-.4.296-.053.053c-.353.352-.547.822-.547 1.321s.194.967.549 1.32c.134.134.288.236.451.322v6.678h14v-16h-11.778z"/><path fill="#E8E8E8" d="M10.798 7l-1.83-2h6.032v2h-4.202zm-2.292-6h-3.207l1.337 1.52 1.87-1.52zm-5.506 8.531v1.469h12v-2h-10.813l-.024.021c-.3.299-.716.479-1.163.51zm0 5.469h12v-2h-12v2zm3.323-8h.631l-.347-.266-.284.266zm8.677-4v-2h-3.289l-1.743 2h5.032z"/><path fill="#F48771" d="M7.246 4.6l2.856-3.277-.405-.002-3.176 2.581-2.607-2.962c-.336-.221-.786-.2-1.082.096-.308.306-.319.779-.069 1.12l2.83 2.444-3.339 2.466c-.339.338-.339.887 0 1.225.339.337.888.337 1.226 0l3.063-2.867 3.33 2.555h.466l-3.093-3.379z"/></svg>
|
||||
|
After Width: | Height: | Size: 787 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M4.222 0h-2.222v.479c-.526.648-.557 1.57-.043 2.269l.043.059v3.203l-.4.296-.053.053c-.353.352-.547.822-.547 1.321s.194.967.549 1.32c.134.134.288.236.451.322v6.678h14v-16h-11.778z"/><path fill="#424242" d="M10.798 7l-1.83-2h6.032v2h-4.202zm-2.292-6h-3.207l1.337 1.52 1.87-1.52zm-5.506 8.531v1.469h12v-2h-10.813l-.024.021c-.3.299-.716.479-1.163.51zm0 5.469h12v-2h-12v2zm3.323-8h.631l-.347-.266-.284.266zm8.677-4v-2h-3.289l-1.743 2h5.032z"/><path fill="#A1260D" d="M7.246 4.6l2.856-3.277-.405-.002-3.176 2.581-2.607-2.962c-.336-.221-.786-.2-1.082.096-.308.306-.319.779-.069 1.12l2.83 2.444-3.339 2.466c-.339.338-.339.887 0 1.225.339.337.888.337 1.226 0l3.063-2.867 3.33 2.555h.466l-3.093-3.379z"/></svg>
|
||||
|
After Width: | Height: | Size: 784 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.connection-label {
|
||||
width: 80px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.connection-input {
|
||||
width: 200px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.connection-dialog {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.connection-dialog .tabbedPanel {
|
||||
border-top-color: transparent;
|
||||
flex: 1 1;
|
||||
min-height: 120px;
|
||||
overflow: hidden;
|
||||
margin: 0px 11px;
|
||||
}
|
||||
|
||||
.connection-dialog .tabBody {
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.connection-recent, .connection-saved {
|
||||
margin: 5px;
|
||||
flex: 1 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.no-recent-connections, .no-saved-connections {
|
||||
font-size: 12px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.connection-history-label {
|
||||
font-size: 15px;
|
||||
display: inline;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.recent-titles-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.connection-provider-info {
|
||||
overflow-y: hidden;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.connection-recent-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.connection-recent-content .server-explorer-viewlet {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.connection-table-content {
|
||||
width:100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.connection-saved-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.connection-type {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
margin: 0px 13px;
|
||||
}
|
||||
|
||||
.vs-dark .connection-dialog .connection-history-actions .action-label.icon,
|
||||
.hc-black .connection-dialog .connection-history-actions .action-label.icon,
|
||||
.connection-dialog .connection-history-actions .action-label.icon {
|
||||
display: block;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
min-width: 20px;
|
||||
background-size: 16px;
|
||||
background-position: 2px center;
|
||||
background-repeat: no-repeat;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.search-action.clear-search-results {
|
||||
background: url('clear-search-results.svg');
|
||||
}
|
||||
|
||||
.vs-dark .search-action.clear-search-results,
|
||||
.hc-black .search-action.clear-search-results {
|
||||
background: url('clear-search-results-dark.svg');
|
||||
}
|
||||
|
||||
.connection-details-title {
|
||||
font-size: 14px;
|
||||
margin: 5px 0px;
|
||||
padding: 5px 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hide-azure-accounts .azure-account-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-username-password .username-password-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-refresh-link .azure-account-row.refresh-credentials-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-azure-tenants .azure-tenant-row {
|
||||
display: none;
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.advanced-button {
|
||||
width: 100px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INewConnectionParams, IConnectionResult, IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
|
||||
export const IConnectionDialogService = createDecorator<IConnectionDialogService>('connectionDialogService');
|
||||
export interface IConnectionDialogService {
|
||||
_serviceBrand: any;
|
||||
/**
|
||||
* Opens the connection dialog and returns the promise for successfully opening the dialog
|
||||
* @param connectionManagementService
|
||||
* @param params
|
||||
* @param model
|
||||
* @param connectionResult
|
||||
*/
|
||||
showDialog(connectionManagementService: IConnectionManagementService, params: INewConnectionParams, model: IConnectionProfile, connectionResult?: IConnectionResult): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Opens the connection dialog and returns the promise when connection is made
|
||||
* or dialog is closed
|
||||
* @param connectionManagementService
|
||||
* @param params
|
||||
* @param model
|
||||
* @param connectionResult
|
||||
*/
|
||||
openDialogAndWait(connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Thenable<IConnectionProfile>;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
.extension-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;
|
||||
}
|
||||
|
||||
.extension-view .header.collapsible.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.extension-view .header .title {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.extension-view .header .count-badge-wrapper {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.extension-view .extensionTab-view .list-row {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.extension-view .extensionTab-view .list-row .extension-status-icon {
|
||||
flex: 0 0 20px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details .title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre-wrap;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details .description {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.extension-view .extensionTab-view .list-row.extensionTab-list .extension-details .publisher {
|
||||
font-size: 90%;
|
||||
padding-right: 6px;
|
||||
opacity: .6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.no-extensionTab-label {
|
||||
font-size: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./media/newDashboardTabDialog';
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { attachModalDialogStyler, attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import { NewDashboardTabViewModel, IDashboardUITab } from 'sql/workbench/services/dashboard/common/newDashboardTabViewModel';
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
|
||||
class ExtensionListDelegate implements IVirtualDelegate<IDashboardUITab> {
|
||||
|
||||
constructor(
|
||||
private _height: number
|
||||
) {
|
||||
}
|
||||
|
||||
public getHeight(element: IDashboardUITab): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
public getTemplateId(element: IDashboardUITab): string {
|
||||
return 'extensionListRenderer';
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtensionListTemplate {
|
||||
root: HTMLElement;
|
||||
icon: HTMLElement;
|
||||
title: HTMLElement;
|
||||
description: HTMLElement;
|
||||
publisher: HTMLElement;
|
||||
}
|
||||
|
||||
class ExtensionListRenderer implements IRenderer<IDashboardUITab, ExtensionListTemplate> {
|
||||
public static TEMPLATE_ID = 'extensionListRenderer';
|
||||
private static readonly OPENED_TAB_CLASS = 'success';
|
||||
private static readonly ICON_CLASS = 'extension-status-icon icon';
|
||||
|
||||
public get templateId(): string {
|
||||
return ExtensionListRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
public renderTemplate(container: HTMLElement): ExtensionListTemplate {
|
||||
const tableTemplate: ExtensionListTemplate = Object.create(null);
|
||||
tableTemplate.root = DOM.append(container, DOM.$('div.list-row.extensionTab-list'));
|
||||
tableTemplate.icon = DOM.append(tableTemplate.root, DOM.$('div.icon'));
|
||||
var titleContainer = DOM.append(tableTemplate.root, DOM.$('div.extension-details'));
|
||||
tableTemplate.title = DOM.append(titleContainer, DOM.$('div.title'));
|
||||
tableTemplate.description = DOM.append(titleContainer, DOM.$('div.description'));
|
||||
tableTemplate.publisher = DOM.append(titleContainer, DOM.$('div.publisher'));
|
||||
return tableTemplate;
|
||||
}
|
||||
|
||||
public renderElement(dashboardTab: IDashboardUITab, index: number, templateData: ExtensionListTemplate): void {
|
||||
templateData.icon.className = ExtensionListRenderer.ICON_CLASS;
|
||||
if (dashboardTab.isOpened) {
|
||||
templateData.icon.classList.add(ExtensionListRenderer.OPENED_TAB_CLASS);
|
||||
}
|
||||
templateData.title.innerText = dashboardTab.tabConfig.title;
|
||||
templateData.description.innerText = dashboardTab.tabConfig.description;
|
||||
templateData.publisher.innerText = dashboardTab.tabConfig.publisher;
|
||||
}
|
||||
|
||||
public disposeTemplate(template: ExtensionListTemplate): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
public disposeElement(element: IDashboardUITab, index: number, templateData: ExtensionListTemplate): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export class NewDashboardTabDialog extends Modal {
|
||||
public static EXTENSIONLIST_HEIGHT = 101;
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _addNewTabButton: Button;
|
||||
private _cancelButton: Button;
|
||||
private _extensionList: List<IDashboardUITab>;
|
||||
private _extensionViewContainer: HTMLElement;
|
||||
private _noExtensionViewContainer: HTMLElement;
|
||||
|
||||
private _viewModel: NewDashboardTabViewModel;
|
||||
|
||||
// EVENTING ////////////////////////////////////////////////////////////
|
||||
private _onAddTabs: Emitter<Array<IDashboardUITab>>;
|
||||
public get onAddTabs(): Event<Array<IDashboardUITab>> { return this._onAddTabs.event; }
|
||||
|
||||
private _onCancel: Emitter<void>;
|
||||
public get onCancel(): Event<void> { return this._onCancel.event; }
|
||||
|
||||
constructor(
|
||||
@IPartService partService: IPartService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IClipboardService clipboardService: IClipboardService
|
||||
) {
|
||||
super(
|
||||
localize('newDashboardTab.openDashboardExtensions', 'Open dashboard extensions'),
|
||||
TelemetryKeys.AddNewDashboardTab,
|
||||
partService,
|
||||
telemetryService,
|
||||
clipboardService,
|
||||
themeService,
|
||||
contextKeyService,
|
||||
{ hasSpinner: true }
|
||||
);
|
||||
|
||||
// Setup the event emitters
|
||||
this._onAddTabs = new Emitter<IDashboardUITab[]>();
|
||||
this._onCancel = new Emitter<void>();
|
||||
|
||||
this._viewModel = new NewDashboardTabViewModel();
|
||||
this._register(this._viewModel.updateTabListEvent(tabs => this.onUpdateTabList(tabs)));
|
||||
}
|
||||
|
||||
// MODAL OVERRIDE METHODS //////////////////////////////////////////////
|
||||
protected layout(height?: number): void {
|
||||
this._extensionList.layout(height);
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
|
||||
this._addNewTabButton = this.addFooterButton(localize('newDashboardTab.ok', 'OK'), () => this.addNewTabs());
|
||||
this._cancelButton = this.addFooterButton(localize('newDashboardTab.cancel', 'Cancel'), () => this.cancel());
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
this._extensionViewContainer = DOM.$('div.extension-view');
|
||||
DOM.append(container, this._extensionViewContainer);
|
||||
|
||||
this.createExtensionList(this._extensionViewContainer);
|
||||
this._noExtensionViewContainer = DOM.$('.no-extension-view');
|
||||
let noExtensionTitle = DOM.append(this._noExtensionViewContainer, DOM.$('.no-extensionTab-label'));
|
||||
let noExtensionLabel = localize('newdashboardTabDialog.noExtensionLabel', 'No dashboard extensions are installed at this time. Go to Extension Manager to explore recommended extensions.');
|
||||
noExtensionTitle.textContent = noExtensionLabel;
|
||||
|
||||
DOM.append(container, this._noExtensionViewContainer);
|
||||
}
|
||||
|
||||
private createExtensionList(container: HTMLElement) {
|
||||
// Create a fixed list view for the extensions
|
||||
let extensionTabViewContainer = DOM.$('.extensionTab-view');
|
||||
let delegate = new ExtensionListDelegate(NewDashboardTabDialog.EXTENSIONLIST_HEIGHT);
|
||||
let extensionTabRenderer = new ExtensionListRenderer();
|
||||
this._extensionList = new List<IDashboardUITab>(extensionTabViewContainer, delegate, [extensionTabRenderer]);
|
||||
|
||||
this._extensionList.onMouseDblClick(e => this.onAccept());
|
||||
this._extensionList.onKeyDown(e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyCode.Enter)) {
|
||||
this.onAccept();
|
||||
} else if (event.equals(KeyCode.Escape)) {
|
||||
this.onClose();
|
||||
}
|
||||
});
|
||||
|
||||
DOM.append(container, extensionTabViewContainer);
|
||||
|
||||
this._register(attachListStyler(this._extensionList, this._themeService));
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
// Theme styler
|
||||
this._register(attachButtonStyler(this._cancelButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._addNewTabButton, this._themeService));
|
||||
}
|
||||
|
||||
/* Overwrite escape key behavior */
|
||||
protected onClose() {
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
/* Overwrite enter key behavior */
|
||||
protected onAccept() {
|
||||
this.addNewTabs();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private addNewTabs() {
|
||||
if (this._addNewTabButton.enabled) {
|
||||
let selectedTabs = this._extensionList.getSelectedElements();
|
||||
this._onAddTabs.fire(selectedTabs);
|
||||
}
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
public open(dashboardTabs: Array<IDashboardTab>, openedTabs: Array<IDashboardTab>) {
|
||||
this.show();
|
||||
this._viewModel.updateDashboardTabs(dashboardTabs, openedTabs);
|
||||
}
|
||||
|
||||
private onUpdateTabList(tabs: IDashboardUITab[]) {
|
||||
this._extensionList.splice(0, this._extensionList.length, tabs);
|
||||
this.layout();
|
||||
if (this._extensionList.length > 0) {
|
||||
this._extensionViewContainer.hidden = false;
|
||||
this._noExtensionViewContainer.hidden = true;
|
||||
this._extensionList.setSelection([0]);
|
||||
this._extensionList.domFocus();
|
||||
this._addNewTabButton.enabled = true;
|
||||
} else {
|
||||
this._extensionViewContainer.hidden = true;
|
||||
this._noExtensionViewContainer.hidden = false;
|
||||
this._addNewTabButton.enabled = false;
|
||||
this._cancelButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { INewDashboardTabDialogService } from 'sql/workbench/services/dashboard/common/newDashboardTabDialog';
|
||||
import { NewDashboardTabDialog } from 'sql/workbench/services/dashboard/browser/newDashboardTabDialog';
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
import { IAngularEventingService, AngularEventType } from 'sql/platform/angularEventing/common/angularEventingService';
|
||||
import { IDashboardUITab } from 'sql/workbench/services/dashboard/common/newDashboardTabViewModel';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class NewDashboardTabDialogService implements INewDashboardTabDialogService {
|
||||
_serviceBrand: any;
|
||||
|
||||
// MEMBER VARIABLES ////////////////////////////////////////////////////
|
||||
private _addNewTabDialog: NewDashboardTabDialog;
|
||||
private _uri: string;
|
||||
|
||||
constructor(
|
||||
@IAngularEventingService private _angularEventService: IAngularEventingService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Open account dialog
|
||||
*/
|
||||
public showDialog(dashboardTabs: Array<IDashboardTab>, openedTabs: Array<IDashboardTab>, uri: string): void {
|
||||
this._uri = uri;
|
||||
let self = this;
|
||||
|
||||
// Create a new dialog if one doesn't exist
|
||||
if (!this._addNewTabDialog) {
|
||||
this._addNewTabDialog = this._instantiationService.createInstance(NewDashboardTabDialog);
|
||||
this._addNewTabDialog.onCancel(() => { self.handleOnCancel(); });
|
||||
this._addNewTabDialog.onAddTabs((selectedTabs) => { self.handleOnAddTabs(selectedTabs); });
|
||||
this._addNewTabDialog.render();
|
||||
}
|
||||
|
||||
// Open the dialog
|
||||
this._addNewTabDialog.open(dashboardTabs, openedTabs);
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private handleOnAddTabs(selectedUiTabs: Array<IDashboardUITab>): void {
|
||||
let selectedTabs = selectedUiTabs.map(tab => tab.tabConfig);
|
||||
this._angularEventService.sendAngularEvent(this._uri, AngularEventType.NEW_TABS, { dashboardTabs: selectedTabs });
|
||||
this._addNewTabDialog.close();
|
||||
}
|
||||
|
||||
private handleOnCancel(): void { }
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
|
||||
export const INewDashboardTabDialogService = createDecorator<INewDashboardTabDialogService>('addNewDashboardTabService');
|
||||
export interface INewDashboardTabDialogService {
|
||||
_serviceBrand: any;
|
||||
showDialog(dashboardTabs: Array<IDashboardTab>, openedTabs: Array<IDashboardTab>, uri: string): void;
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
import { IDashboardTab } from 'sql/platform/dashboard/common/dashboardRegistry';
|
||||
|
||||
|
||||
export interface IDashboardUITab {
|
||||
tabConfig: IDashboardTab;
|
||||
isOpened?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* View model for new dashboard tab
|
||||
*/
|
||||
export class NewDashboardTabViewModel {
|
||||
|
||||
// EVENTING ///////////////////////////////////////////////////////
|
||||
private _updateTabListEmitter: Emitter<IDashboardUITab[]>;
|
||||
public get updateTabListEvent(): Event<IDashboardUITab[]> { return this._updateTabListEmitter.event; }
|
||||
|
||||
|
||||
constructor() {
|
||||
// Create event emitters
|
||||
this._updateTabListEmitter = new Emitter<IDashboardUITab[]>();
|
||||
}
|
||||
|
||||
public updateDashboardTabs(dashboardTabs: Array<IDashboardTab>, openedTabs: Array<IDashboardTab>) {
|
||||
let tabList: IDashboardUITab[] = [];
|
||||
dashboardTabs.forEach(tab => {
|
||||
tabList.push({ tabConfig: tab });
|
||||
});
|
||||
openedTabs.forEach(tab => {
|
||||
let uiTab = tabList.find(i => i.tabConfig === tab);
|
||||
if (uiTab) {
|
||||
uiTab.isOpened = true;
|
||||
}
|
||||
});
|
||||
this._updateTabListEmitter.fire(tabList);
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./media/errorMessageDialog';
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { Modal } from 'sql/base/browser/ui/modal/modal';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import { attachButtonStyler, attachModalDialogStyler } from 'sql/common/theme/styler';
|
||||
import { attachButtonStyler, attachModalDialogStyler } from 'sql/platform/theme/common/styler';
|
||||
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
import treedefaults = require('vs/base/parts/tree/browser/treeDefaults');
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
|
||||
/**
|
||||
* Extends the tree controller to handle mouse and keyboard events on the tree elements
|
||||
*/
|
||||
export class FileBrowserController extends treedefaults.DefaultController {
|
||||
|
||||
constructor() {
|
||||
super({ clickBehavior: treedefaults.ClickBehavior.ON_MOUSE_DOWN, openMode: treedefaults.OpenMode.SINGLE_CLICK });
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: ITree, element: any, event: IMouseEvent, origin: string = 'mouse'): boolean {
|
||||
// In file browser, double clicking an element calls tree.dispose(). There should not be any tree events after selection.
|
||||
if (event.detail === 2) {
|
||||
var payload = { origin: origin, originalEvent: event };
|
||||
if (tree.getInput() === element) {
|
||||
tree.clearFocus(payload);
|
||||
tree.clearSelection(payload);
|
||||
} else {
|
||||
var isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
|
||||
if (!isMouseDown) {
|
||||
event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
|
||||
}
|
||||
event.stopPropagation();
|
||||
tree.domFocus();
|
||||
tree.setSelection([element], payload);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return super.onLeftClick(tree, element, event, origin);
|
||||
}
|
||||
}
|
||||
|
||||
protected onEnter(tree: ITree, event: IKeyboardEvent): boolean {
|
||||
var payload = { origin: 'keyboard', originalEvent: event };
|
||||
|
||||
if (tree.getHighlight()) {
|
||||
return false;
|
||||
}
|
||||
var focus = tree.getFocus();
|
||||
if (focus) {
|
||||
// In file browser, pressing enter key on an element will close dialog and call tree.dispose(). There should not be any tree events after selection.
|
||||
tree.setSelection([focus], payload);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IFileBrowserService } from 'sql/platform/fileBrowser/common/interfaces';
|
||||
import { FileNode } from 'sql/workbench/services/fileBrowser/common/fileNode';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ITree, IDataSource } from 'vs/base/parts/tree/browser/tree';
|
||||
|
||||
/**
|
||||
* Implements the DataSource(that returns a parent/children of an element) for the file browser
|
||||
*/
|
||||
export class FileBrowserDataSource implements IDataSource {
|
||||
|
||||
constructor(
|
||||
@IFileBrowserService private _fileBrowserService: IFileBrowserService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique identifier of the given element.
|
||||
* No more than one element may use a given identifier.
|
||||
*/
|
||||
public getId(tree: ITree, element: any): string {
|
||||
if (element instanceof FileNode) {
|
||||
return (<FileNode>element).id;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether the element has children.
|
||||
*/
|
||||
public hasChildren(tree: ITree, element: any): boolean {
|
||||
if (element instanceof FileNode) {
|
||||
return (<FileNode>element).hasChildren;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element's children as an array in a promise.
|
||||
*/
|
||||
public getChildren(tree: ITree, element: any): TPromise<any> {
|
||||
return new TPromise<any>((resolve) => {
|
||||
if (element instanceof FileNode) {
|
||||
var node = <FileNode>element;
|
||||
if (node.children) {
|
||||
resolve(node.children);
|
||||
} else {
|
||||
this._fileBrowserService.expandFolderNode(node).then((nodeChildren) => {
|
||||
resolve(nodeChildren);
|
||||
}, expandError => {
|
||||
resolve([]);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element's parent in a promise.
|
||||
*/
|
||||
public getParent(tree: ITree, element: any): TPromise<any> {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!./media/fileBrowserDialog';
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { attachModalDialogStyler, attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import { FileNode } from 'sql/workbench/services/fileBrowser/common/fileNode';
|
||||
import { FileBrowserTreeView } from 'sql/workbench/services/fileBrowser/browser/fileBrowserTreeView';
|
||||
import { FileBrowserViewModel } from 'sql/workbench/services/fileBrowser/common/fileBrowserViewModel';
|
||||
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
|
||||
export class FileBrowserDialog extends Modal {
|
||||
private _viewModel: FileBrowserViewModel;
|
||||
private _bodyBuilder: Builder;
|
||||
private _filePathInputBox: InputBox;
|
||||
private _fileFilterSelectBox: SelectBox;
|
||||
private _okButton: Button;
|
||||
private _cancelButton: Button;
|
||||
private _onOk = new Emitter<string>();
|
||||
public onOk: Event<string> = this._onOk.event;
|
||||
|
||||
private _treeContainer: Builder;
|
||||
private _fileBrowserTreeView: FileBrowserTreeView;
|
||||
private _selectedFilePath: string;
|
||||
private _isFolderSelected: boolean;
|
||||
|
||||
constructor(title: string,
|
||||
@IPartService partService: IPartService,
|
||||
@IWorkbenchThemeService private _workbenchthemeService: IWorkbenchThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IClipboardService clipboardService: IClipboardService
|
||||
) {
|
||||
super(title, TelemetryKeys.Backup, partService, telemetryService, clipboardService, _workbenchthemeService, contextKeyService, { isFlyout: true, hasTitleIcon: false, hasBackButton: true, hasSpinner: true });
|
||||
this._viewModel = this._instantiationService.createInstance(FileBrowserViewModel);
|
||||
this._viewModel.onAddFileTree(args => this.handleOnAddFileTree(args.rootNode, args.selectedNode, args.expandedNodes));
|
||||
this._viewModel.onPathValidate(args => this.handleOnValidate(args.succeeded, args.message));
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
new Builder(container).div({ 'class': 'file-browser-dialog' }, (bodyBuilder) => {
|
||||
this._bodyBuilder = bodyBuilder;
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
attachModalDialogStyler(this, this._themeService);
|
||||
|
||||
if (this.backButton) {
|
||||
|
||||
this.backButton.onDidClick(() => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
this._register(attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }));
|
||||
}
|
||||
|
||||
this._bodyBuilder.div({ class: 'tree-view' }, (treeContainer) => {
|
||||
this._treeContainer = treeContainer;
|
||||
});
|
||||
|
||||
this._bodyBuilder.div({ class: 'option-section' }, (tableWrapper) => {
|
||||
tableWrapper.element('table', { class: 'file-table-content' }, (tableContainer) => {
|
||||
let pathLabel = localize('filebrowser.filepath', 'Selected path');
|
||||
let pathBuilder = DialogHelper.appendRow(tableContainer, pathLabel, 'file-input-label', 'file-input-box');
|
||||
this._filePathInputBox = new InputBox(pathBuilder.getHTMLElement(), this._contextViewService, {
|
||||
ariaLabel: pathLabel
|
||||
});
|
||||
|
||||
this._fileFilterSelectBox = new SelectBox(['*'], '*', this._contextViewService);
|
||||
let filterLabel = localize('fileFilter', 'Files of type');
|
||||
let filterBuilder = DialogHelper.appendRow(tableContainer, filterLabel, 'file-input-label', 'file-input-box');
|
||||
DialogHelper.appendInputSelectBox(filterBuilder, this._fileFilterSelectBox);
|
||||
});
|
||||
});
|
||||
|
||||
this._okButton = this.addFooterButton(localize('fileBrowser.ok', 'OK'), () => this.ok());
|
||||
this._okButton.enabled = false;
|
||||
this._cancelButton = this.addFooterButton(localize('fileBrowser.discard', 'Discard'), () => this.close());
|
||||
|
||||
this.registerListeners();
|
||||
this.updateTheme();
|
||||
}
|
||||
|
||||
public open(ownerUri: string,
|
||||
expandPath: string,
|
||||
fileFilters: [{ label: string, filters: string[] }],
|
||||
fileValidationServiceType: string,
|
||||
) {
|
||||
this._viewModel.initialize(ownerUri, expandPath, fileFilters, fileValidationServiceType);
|
||||
this._fileFilterSelectBox.setOptions(this._viewModel.formattedFileFilters);
|
||||
this._fileFilterSelectBox.select(0);
|
||||
this._filePathInputBox.value = expandPath;
|
||||
this._isFolderSelected = true;
|
||||
this.enableOkButton();
|
||||
this.showSpinner();
|
||||
this.show();
|
||||
|
||||
this._fileBrowserTreeView = this._instantiationService.createInstance(FileBrowserTreeView);
|
||||
this._fileBrowserTreeView.setOnClickedCallback((arg) => this.onClicked(arg));
|
||||
this._fileBrowserTreeView.setOnDoubleClickedCallback((arg) => this.onDoubleClicked(arg));
|
||||
this._viewModel.openFileBrowser(0, false);
|
||||
}
|
||||
|
||||
/* enter key */
|
||||
protected onAccept() {
|
||||
if (this._okButton.enabled === true) {
|
||||
this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
private handleOnAddFileTree(rootNode: FileNode, selectedNode: FileNode, expandedNodes: FileNode[]) {
|
||||
this.updateFileTree(rootNode, selectedNode, expandedNodes);
|
||||
this.hideSpinner();
|
||||
}
|
||||
|
||||
private enableOkButton() {
|
||||
if (strings.isFalsyOrWhitespace(this._selectedFilePath) || this._isFolderSelected === true) {
|
||||
this._okButton.enabled = false;
|
||||
} else {
|
||||
this._okButton.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private onClicked(selectedNode: FileNode) {
|
||||
this._filePathInputBox.value = selectedNode.fullPath;
|
||||
|
||||
if (selectedNode.isFile === true) {
|
||||
this._isFolderSelected = false;
|
||||
} else {
|
||||
this._isFolderSelected = true;
|
||||
}
|
||||
|
||||
this.enableOkButton();
|
||||
}
|
||||
|
||||
private onDoubleClicked(selectedNode: FileNode) {
|
||||
if (selectedNode.isFile === true) {
|
||||
this.ok();
|
||||
}
|
||||
}
|
||||
|
||||
private onFilePathChange(filePath: string) {
|
||||
this._isFolderSelected = false;
|
||||
this._selectedFilePath = filePath;
|
||||
|
||||
this._filePathInputBox.hideMessage();
|
||||
this.enableOkButton();
|
||||
}
|
||||
|
||||
private onFilePathBlur(param) {
|
||||
if (!strings.isFalsyOrWhitespace(param.value)) {
|
||||
this._viewModel.validateFilePaths([param.value]);
|
||||
}
|
||||
}
|
||||
|
||||
private ok() {
|
||||
this._onOk.fire(this._selectedFilePath);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private handleOnValidate(succeeded: boolean, errorMessage: string) {
|
||||
if (succeeded === false) {
|
||||
if (strings.isFalsyOrWhitespace(errorMessage)) {
|
||||
errorMessage = 'The provided path is invalid.';
|
||||
}
|
||||
this._filePathInputBox.showMessage({ type: MessageType.ERROR, content: errorMessage });
|
||||
}
|
||||
}
|
||||
|
||||
private close() {
|
||||
if (this._fileBrowserTreeView) {
|
||||
this._fileBrowserTreeView.dispose();
|
||||
}
|
||||
this._onOk.dispose();
|
||||
this.hide();
|
||||
this._viewModel.closeFileBrowser();
|
||||
}
|
||||
|
||||
private updateFileTree(rootNode: FileNode, selectedNode: FileNode, expandedNodes: FileNode[]): void {
|
||||
this._fileBrowserTreeView.renderBody(this._treeContainer.getHTMLElement(), rootNode, selectedNode, expandedNodes);
|
||||
this._fileBrowserTreeView.setVisible(true);
|
||||
this._fileBrowserTreeView.layout(DOM.getTotalHeight(this._treeContainer.getHTMLElement()));
|
||||
}
|
||||
|
||||
private onFilterSelectChanged(filterIndex) {
|
||||
this.showSpinner();
|
||||
this._viewModel.openFileBrowser(filterIndex, true);
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this._fileFilterSelectBox.onDidSelect(selection => {
|
||||
this.onFilterSelectChanged(selection.index);
|
||||
}));
|
||||
this._register(this._filePathInputBox.onDidChange(e => {
|
||||
this.onFilePathChange(e);
|
||||
}));
|
||||
this._register(this._filePathInputBox.onLoseFocus(params => {
|
||||
this.onFilePathBlur(params);
|
||||
}));
|
||||
|
||||
// Theme styler
|
||||
this._register(attachInputBoxStyler(this._filePathInputBox, this._themeService));
|
||||
this._register(attachSelectBoxStyler(this._fileFilterSelectBox, this._themeService));
|
||||
this._register(attachButtonStyler(this._okButton, this._themeService));
|
||||
this._register(attachButtonStyler(this._cancelButton, this._themeService));
|
||||
|
||||
this._register(this._workbenchthemeService.onDidColorThemeChange(e => this.updateTheme()));
|
||||
}
|
||||
|
||||
// Update theming that is specific to file browser
|
||||
private updateTheme(): void {
|
||||
if (this._treeContainer) {
|
||||
this._treeContainer.style('background-color', this.headerAndFooterBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController';
|
||||
import { FileBrowserDialog } from 'sql/workbench/services/fileBrowser/browser/fileBrowserDialog';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
/**
|
||||
* File browser dialog service
|
||||
*/
|
||||
export class FileBrowserDialogController implements IFileBrowserDialogController {
|
||||
_serviceBrand: any;
|
||||
private _fileBrowserDialog: FileBrowserDialog;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
}
|
||||
|
||||
public showDialog(ownerUri: string,
|
||||
expandPath: string,
|
||||
fileFilters: [{ label: string, filters: string[] }],
|
||||
fileValidationServiceType: string,
|
||||
isWide: boolean,
|
||||
handleOnOk: (path: string) => void
|
||||
) {
|
||||
if (!this._fileBrowserDialog) {
|
||||
this._fileBrowserDialog = this._instantiationService.createInstance(FileBrowserDialog, localize('filebrowser.selectFile', "Select a file"));
|
||||
this._fileBrowserDialog.render();
|
||||
}
|
||||
|
||||
this._fileBrowserDialog.setWide(isWide);
|
||||
this._fileBrowserDialog.onOk((filepath) => handleOnOk(filepath));
|
||||
this._fileBrowserDialog.open(ownerUri, expandPath, fileFilters, fileValidationServiceType);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { FileNode } from 'sql/workbench/services/fileBrowser/common/fileNode';
|
||||
import { ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
|
||||
import { FileKind } from 'vs/platform/files/common/files';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { FileLabel } from 'vs/workbench/browser/labels';
|
||||
import { IFileTemplateData } from 'vs/workbench/parts/files/electron-browser/views/explorerViewer';
|
||||
import { toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
const EmptyDisposable = toDisposable(() => null);
|
||||
|
||||
/**
|
||||
* Renders the tree items.
|
||||
* Uses the dom template to render file browser.
|
||||
*/
|
||||
export class FileBrowserRenderer implements IRenderer {
|
||||
public static readonly FILE_HEIGHT = 22;
|
||||
private static readonly FILE_TEMPLATE_ID = 'carbonFileBrowser';
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element's height in the tree, in pixels.
|
||||
*/
|
||||
public getHeight(tree: ITree, element: any): number {
|
||||
return FileBrowserRenderer.FILE_HEIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a template ID for a given element.
|
||||
*/
|
||||
public getTemplateId(tree: ITree, element: any): string {
|
||||
return FileBrowserRenderer.FILE_TEMPLATE_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render template in a dom element based on template id
|
||||
*/
|
||||
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileTemplateData {
|
||||
const elementDisposable = EmptyDisposable;
|
||||
const label = this.instantiationService.createInstance(FileLabel, container, void 0);
|
||||
return { elementDisposable, label, container };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a element, given an object bag returned by the template
|
||||
*/
|
||||
public renderElement(tree: ITree, element: FileNode, templateId: string, templateData: IFileTemplateData): void {
|
||||
if (element) {
|
||||
templateData.label.element.style.display = 'flex';
|
||||
const extraClasses = ['explorer-item'];
|
||||
|
||||
var fileuri = URI.file(element.fullPath);
|
||||
var filekind;
|
||||
if (element.parent === null) {
|
||||
filekind = FileKind.ROOT_FOLDER;
|
||||
} else if (element.isFile === false) {
|
||||
filekind = FileKind.FOLDER;
|
||||
} else {
|
||||
filekind = FileKind.FILE;
|
||||
}
|
||||
|
||||
templateData.label.setFile(fileuri, { hidePath: true, fileKind: filekind, extraClasses });
|
||||
}
|
||||
}
|
||||
|
||||
public disposeTemplate(tree: ITree, templateId: string, templateData: IFileTemplateData): void {
|
||||
templateData.label.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { FileBrowserDataSource } from 'sql/workbench/services/fileBrowser/browser/fileBrowserDataSource';
|
||||
import { FileBrowserController } from 'sql/workbench/services/fileBrowser/browser/fileBrowserController';
|
||||
import { FileBrowserRenderer } from 'sql/workbench/services/fileBrowser/browser/fileBrowserRenderer';
|
||||
import { IFileBrowserService } from 'sql/platform/fileBrowser/common/interfaces';
|
||||
import { FileNode } from 'sql/workbench/services/fileBrowser/common/fileNode';
|
||||
import errors = require('vs/base/common/errors');
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import nls = require('vs/nls');
|
||||
import { DefaultFilter, DefaultAccessibilityProvider, DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
|
||||
/**
|
||||
* Implements tree view for file browser
|
||||
*/
|
||||
export class FileBrowserTreeView implements IDisposable {
|
||||
private _tree: ITree;
|
||||
private _toDispose: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IFileBrowserService private _fileBrowserService: IFileBrowserService,
|
||||
@IThemeService private _themeService: IThemeService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the view body
|
||||
*/
|
||||
public renderBody(container: HTMLElement, rootNode: FileNode, selectedNode: FileNode, expandedNodes: FileNode[]): void {
|
||||
if (!this._tree) {
|
||||
DOM.addClass(container, 'show-file-icons');
|
||||
this._tree = this.createFileBrowserTree(container, this._instantiationService);
|
||||
this._toDispose.push(this._tree.onDidChangeSelection((event) => this.onSelected(event)));
|
||||
this._toDispose.push(this._fileBrowserService.onExpandFolder(fileNode => this._tree.refresh(fileNode)));
|
||||
this._toDispose.push(attachListStyler(this._tree, this._themeService));
|
||||
this._tree.domFocus();
|
||||
}
|
||||
|
||||
if (rootNode) {
|
||||
this._tree.setInput(rootNode).then(() => {
|
||||
if (expandedNodes) {
|
||||
this._tree.expandAll(expandedNodes);
|
||||
}
|
||||
if (selectedNode) {
|
||||
this._tree.select(selectedNode);
|
||||
this._tree.setFocus(selectedNode);
|
||||
}
|
||||
this._tree.getFocus();
|
||||
}, errors.onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file browser tree
|
||||
*/
|
||||
public createFileBrowserTree(treeContainer: HTMLElement, instantiationService: IInstantiationService): Tree {
|
||||
const dataSource = instantiationService.createInstance(FileBrowserDataSource);
|
||||
const renderer = instantiationService.createInstance(FileBrowserRenderer);
|
||||
const controller = instantiationService.createInstance(FileBrowserController);
|
||||
const dnd = new DefaultDragAndDrop();
|
||||
const filter = new DefaultFilter();
|
||||
const sorter = null;
|
||||
const accessibilityProvider = new DefaultAccessibilityProvider();
|
||||
|
||||
return new Tree(treeContainer, {
|
||||
dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider
|
||||
}, {
|
||||
indentPixels: 10,
|
||||
twistiePixels: 12,
|
||||
ariaLabel: nls.localize({ key: 'fileBrowser.regTreeAriaLabel', comment: ['FileBrowserTree'] }, 'File browser tree')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the tree
|
||||
*/
|
||||
public refreshTree(rootNode: FileNode): void {
|
||||
let selectedElement: any;
|
||||
let targetsToExpand: any[];
|
||||
|
||||
// Focus
|
||||
this._tree.domFocus();
|
||||
|
||||
if (this._tree) {
|
||||
let selection = this._tree.getSelection();
|
||||
if (selection && selection.length === 1) {
|
||||
selectedElement = <any>selection[0];
|
||||
}
|
||||
targetsToExpand = this._tree.getExpandedElements();
|
||||
}
|
||||
|
||||
if (rootNode) {
|
||||
this._tree.setInput(rootNode).then(() => {
|
||||
// Make sure to expand all folders that were expanded in the previous session
|
||||
if (targetsToExpand) {
|
||||
this._tree.expandAll(targetsToExpand);
|
||||
}
|
||||
if (selectedElement) {
|
||||
this._tree.select(selectedElement);
|
||||
this._tree.setFocus(selectedElement);
|
||||
}
|
||||
this._tree.getFocus();
|
||||
}, errors.onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
private onSelected(event: any) {
|
||||
let selection = this._tree.getSelection();
|
||||
|
||||
if (selection && selection.length > 0 && (selection[0] instanceof FileNode)) {
|
||||
let isMouseOrigin = event.payload && (event.payload.origin === 'mouse');
|
||||
let isSingleClick = isMouseOrigin && event.payload.originalEvent && event.payload.originalEvent.detail === 1;
|
||||
let isDoubleClick = isMouseOrigin && event.payload.originalEvent && event.payload.originalEvent.detail === 2;
|
||||
if (isSingleClick) {
|
||||
this.onClickedCallback(event.selection[0]);
|
||||
} else if (isDoubleClick) {
|
||||
this.onDoublieClickedCallback(event.selection[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onClickedCallback: any;
|
||||
public setOnClickedCallback(fn: any) {
|
||||
this.onClickedCallback = fn;
|
||||
}
|
||||
|
||||
public onDoublieClickedCallback: any;
|
||||
public setOnDoubleClickedCallback(fn: any) {
|
||||
this.onDoublieClickedCallback = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the layout of the view
|
||||
*/
|
||||
public layout(height?: number): void {
|
||||
this._tree.layout(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* set the visibility of the view
|
||||
*/
|
||||
public setVisible(visible: boolean): void {
|
||||
if (visible) {
|
||||
this._tree.onVisible();
|
||||
} else {
|
||||
this._tree.onHidden();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* dispose the file browser tree view
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this._tree) {
|
||||
this._tree.dispose();
|
||||
}
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.file-browser-dialog {
|
||||
height: 100%;
|
||||
padding-top: 12px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.file-browser-dialog .tree-view {
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.file-table-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-browser-dialog .option-section {
|
||||
padding-top: 10px;
|
||||
height: 90px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
width: 50px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.file-input-box {
|
||||
width: 200px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.file-browser-dialog .explorer-item {
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.file-browser-dialog .show-file-icons .monaco-tree-row .content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.file-browser-dialog .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
|
||||
background-size: 16px;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 6px;
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
content: ' ';
|
||||
position: initial;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const IFileBrowserDialogController = createDecorator<IFileBrowserDialogController>('fileBrowserDialogService');
|
||||
export interface IFileBrowserDialogController {
|
||||
_serviceBrand: any;
|
||||
/**
|
||||
* Show file browser dialog
|
||||
*/
|
||||
showDialog(ownerUri: string,
|
||||
expandPath: string,
|
||||
fileFilters: { label: string, filters: string[] }[],
|
||||
fileValidationServiceType: string,
|
||||
isWide: boolean,
|
||||
handleOnOk: (path: string) => void): void;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { FileNode } from 'sql/workbench/services/fileBrowser/common/fileNode';
|
||||
|
||||
/**
|
||||
* File tree info needed to render initially
|
||||
*/
|
||||
export class FileBrowserTree {
|
||||
public rootNode: FileNode;
|
||||
public selectedNode: FileNode;
|
||||
public expandedNodes: FileNode[];
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IFileBrowserService } from 'sql/platform/fileBrowser/common/interfaces';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
/**
|
||||
* View model for file browser dialog
|
||||
*/
|
||||
export class FileBrowserViewModel {
|
||||
private _ownerUri: string;
|
||||
private _expandPath: string;
|
||||
private _fileFilters: [{ label: string, filters: string[] }];
|
||||
private _fileValidationServiceType: string;
|
||||
public formattedFileFilters: string[];
|
||||
|
||||
constructor( @IFileBrowserService private _fileBrowserService: IFileBrowserService) {
|
||||
}
|
||||
|
||||
public onAddFileTree(onAddFileTreeCallback) {
|
||||
this._fileBrowserService.onAddFileTree(args => onAddFileTreeCallback(args));
|
||||
}
|
||||
|
||||
public onPathValidate(onPathValidateCallback) {
|
||||
this._fileBrowserService.onPathValidate(args => onPathValidateCallback(args));
|
||||
}
|
||||
|
||||
public initialize(ownerUri: string,
|
||||
expandPath: string,
|
||||
fileFilters: [{ label: string, filters: string[] }],
|
||||
fileValidationServiceType: string,
|
||||
) {
|
||||
this._ownerUri = ownerUri;
|
||||
this._expandPath = expandPath;
|
||||
this._fileValidationServiceType = fileValidationServiceType;
|
||||
|
||||
if (!fileFilters) {
|
||||
this._fileFilters = [{ label: localize('allFiles', 'All files'), filters: ['*'] }];
|
||||
} else {
|
||||
this._fileFilters = fileFilters;
|
||||
}
|
||||
this.formattedFileFilters = [];
|
||||
for (var i = 0; i < this._fileFilters.length; i++) {
|
||||
var filterStr = this._fileFilters[i].label + '(' + this._fileFilters[i].filters.join(';') + ')';
|
||||
this.formattedFileFilters.push(filterStr);
|
||||
}
|
||||
}
|
||||
|
||||
public validateFilePaths(selectedFiles: string[]) {
|
||||
this._fileBrowserService.validateFilePaths(this._ownerUri, this._fileValidationServiceType, selectedFiles);
|
||||
}
|
||||
|
||||
public openFileBrowser(filterIndex: number, changeFilter: boolean) {
|
||||
if (this._fileFilters[filterIndex]) {
|
||||
this._fileBrowserService.openFileBrowser(this._ownerUri, this._expandPath, this._fileFilters[filterIndex].filters, changeFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public closeFileBrowser() {
|
||||
this._fileBrowserService.closeFileBrowser(this._ownerUri);
|
||||
}
|
||||
}
|
||||
72
src/sql/workbench/services/fileBrowser/common/fileNode.ts
Normal file
72
src/sql/workbench/services/fileBrowser/common/fileNode.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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
|
||||
/**
|
||||
* File/folder node in file browser
|
||||
* FileTreeNode is converted to this FileNode for UI interactions
|
||||
*/
|
||||
export class FileNode {
|
||||
/**
|
||||
* Node id
|
||||
*/
|
||||
public id: string;
|
||||
|
||||
/**
|
||||
* Connection uri
|
||||
*/
|
||||
public ownerUri: string;
|
||||
|
||||
/**
|
||||
* File or folder name
|
||||
*/
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* Full path of file or folder
|
||||
*/
|
||||
public fullPath: string;
|
||||
|
||||
/**
|
||||
* Parent node
|
||||
*/
|
||||
public parent: FileNode;
|
||||
|
||||
/**
|
||||
* Children nodes
|
||||
*/
|
||||
public children: FileNode[];
|
||||
|
||||
/**
|
||||
* Is the node expanded
|
||||
*/
|
||||
public isExpanded: boolean;
|
||||
|
||||
/**
|
||||
* Is the node file or folder
|
||||
*/
|
||||
public isFile: boolean;
|
||||
|
||||
/**
|
||||
* Does this node have children
|
||||
*/
|
||||
public hasChildren: boolean;
|
||||
|
||||
constructor(id: string, name: string, fullPath: string, isFile: boolean, isExpanded: boolean, ownerUri: string, parent: FileNode) {
|
||||
if (id) {
|
||||
this.id = id;
|
||||
} else {
|
||||
this.id = generateUuid();
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.fullPath = fullPath;
|
||||
this.isFile = isFile;
|
||||
this.ownerUri = ownerUri;
|
||||
this.isExpanded = isExpanded;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* List of services that provide file validation callback to file browser service
|
||||
*/
|
||||
|
||||
export const backup: string = 'Backup';
|
||||
export const restore: string = 'Restore';
|
||||
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { InsightsDialogController } from 'sql/workbench/services/insights/node/insightsDialogController';
|
||||
import { InsightsDialogView } from 'sql/workbench/services/insights/browser/insightsDialogView';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IInsightsDialogModel, IInsightsDialogService } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
import { InsightsDialogModel } from 'sql/workbench/services/insights/common/insightsDialogModel';
|
||||
|
||||
export class InsightsDialogService implements IInsightsDialogService {
|
||||
_serviceBrand: any;
|
||||
private _insightsDialogController: InsightsDialogController;
|
||||
private _insightsDialogView: InsightsDialogView;
|
||||
private _insightsDialogModel: IInsightsDialogModel;
|
||||
|
||||
constructor( @IInstantiationService private _instantiationService: IInstantiationService) { }
|
||||
|
||||
// query string
|
||||
public show(input: IInsightsConfig, connectionProfile: IConnectionProfile): void {
|
||||
if (!this._insightsDialogView) {
|
||||
this._insightsDialogModel = new InsightsDialogModel();
|
||||
this._insightsDialogController = this._instantiationService.createInstance(InsightsDialogController, this._insightsDialogModel);
|
||||
this._insightsDialogView = this._instantiationService.createInstance(InsightsDialogView, this._insightsDialogModel);
|
||||
this._insightsDialogView.render();
|
||||
} else {
|
||||
this._insightsDialogModel.reset();
|
||||
this._insightsDialogView.reset();
|
||||
}
|
||||
|
||||
this._insightsDialogModel.insight = input.details;
|
||||
this._insightsDialogController.update(input.details, connectionProfile);
|
||||
this._insightsDialogView.open(input.details, connectionProfile);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._insightsDialogView.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/insightsDialog';
|
||||
|
||||
import { Button } from 'sql/base/browser/ui/button/button';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { Modal } from 'sql/workbench/browser/modal/modal';
|
||||
import { IInsightsConfigDetails } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { attachButtonStyler, attachModalDialogStyler, attachTableStyler, attachPanelStyler } from 'sql/platform/theme/common/styler';
|
||||
import { TaskRegistry } from 'sql/platform/tasks/common/tasks';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import { IInsightsDialogModel, ListResource, IInsightDialogActionContext, insertValueRegex } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { CopyInsightDialogSelectionAction } from 'sql/workbench/services/insights/common/insightDialogActions';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { MenuRegistry, ExecuteCommandAction } from 'vs/platform/actions/common/actions';
|
||||
import { SplitView, Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
const labelDisplay = nls.localize("insights.item", "Item");
|
||||
const valueDisplay = nls.localize("insights.value", "Value");
|
||||
|
||||
class InsightTableView<T> extends ViewletPanel {
|
||||
private _table: Table<T>;
|
||||
public get table(): Table<T> {
|
||||
return this._table;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private columns: Slick.Column<T>[],
|
||||
private data: IDisposableDataProvider<T> | Array<T>,
|
||||
private tableOptions: Slick.GridOptions<T>,
|
||||
options: IViewletPanelOptions,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService);
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this._table = new Table(container, {
|
||||
columns: this.columns,
|
||||
dataProvider: this.data
|
||||
}, this.tableOptions);
|
||||
}
|
||||
|
||||
protected layoutBody(size: number): void {
|
||||
this._table.layout(size, Orientation.VERTICAL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function stateFormatter(row: number, cell: number, value: any, columnDef: Slick.Column<ListResource>, resource: ListResource): string {
|
||||
// template
|
||||
const icon = DOM.$('span.icon-span');
|
||||
const badge = DOM.$('div.badge');
|
||||
const badgeContent = DOM.$('div.badge-content');
|
||||
DOM.append(badge, badgeContent);
|
||||
DOM.append(icon, badge);
|
||||
|
||||
// render icon if passed
|
||||
if (resource.icon) {
|
||||
icon.classList.add('icon');
|
||||
icon.classList.add(resource.icon);
|
||||
} else {
|
||||
icon.classList.remove('icon');
|
||||
}
|
||||
|
||||
//render state badge if present
|
||||
if (resource.stateColor) {
|
||||
badgeContent.style.backgroundColor = resource.stateColor;
|
||||
badgeContent.classList.remove('icon');
|
||||
} else if (resource.stateIcon) {
|
||||
badgeContent.style.backgroundColor = '';
|
||||
badgeContent.classList.add('icon');
|
||||
badgeContent.classList.add(resource.stateIcon);
|
||||
} else {
|
||||
badgeContent.classList.remove('icon');
|
||||
badgeContent.style.backgroundColor = '';
|
||||
}
|
||||
|
||||
return icon.outerHTML;
|
||||
}
|
||||
|
||||
export class InsightsDialogView extends Modal {
|
||||
|
||||
private _connectionProfile: IConnectionProfile;
|
||||
private _insight: IInsightsConfigDetails;
|
||||
private _splitView: SplitView;
|
||||
private _container: HTMLElement;
|
||||
private _closeButton: Button;
|
||||
private _topTable: Table<ListResource>;
|
||||
private _topTableData: TableDataView<ListResource>;
|
||||
private _bottomTable: Table<ListResource>;
|
||||
private _bottomTableData: TableDataView<ListResource>;
|
||||
private _taskButtonDisposables: IDisposable[] = [];
|
||||
private _topColumns: Array<Slick.Column<ListResource>> = [
|
||||
{
|
||||
name: '',
|
||||
field: 'state',
|
||||
id: 'state',
|
||||
width: 20,
|
||||
resizable: false,
|
||||
formatter: stateFormatter
|
||||
},
|
||||
{
|
||||
name: labelDisplay,
|
||||
field: 'label',
|
||||
id: 'label'
|
||||
},
|
||||
{
|
||||
name: valueDisplay,
|
||||
field: 'value',
|
||||
id: 'value'
|
||||
}
|
||||
];
|
||||
|
||||
private _bottomColumns: Array<Slick.Column<ListResource>> = [
|
||||
{
|
||||
name: nls.localize("property", "Property"),
|
||||
field: 'label',
|
||||
id: 'label'
|
||||
},
|
||||
{
|
||||
name: nls.localize("value", "Value"),
|
||||
field: 'value',
|
||||
id: 'value'
|
||||
}
|
||||
];
|
||||
|
||||
constructor(
|
||||
private _model: IInsightsDialogModel,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IPartService partService: IPartService,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService private _commandService: ICommandService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||
@IClipboardService clipboardService: IClipboardService
|
||||
) {
|
||||
super(nls.localize("InsightsDialogTitle", "Insights"), TelemetryKeys.Insights, partService, telemetryService, clipboardService, themeService, contextKeyService);
|
||||
this._model.onDataChange(e => this.build());
|
||||
}
|
||||
|
||||
private updateTopColumns(): void {
|
||||
let labelName = this.labelColumnName ? this.labelColumnName : labelDisplay;
|
||||
let valueName = this._insight.value ? this._insight.value : valueDisplay;
|
||||
this._topColumns = [
|
||||
{
|
||||
name: '',
|
||||
field: 'state',
|
||||
id: 'state',
|
||||
width: 20,
|
||||
resizable: false,
|
||||
formatter: stateFormatter
|
||||
},
|
||||
{
|
||||
name: labelName,
|
||||
field: 'label',
|
||||
id: 'label'
|
||||
},
|
||||
{
|
||||
name: valueName,
|
||||
field: 'value',
|
||||
id: 'value'
|
||||
}
|
||||
];
|
||||
this._topTable.columns = this._topColumns;
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement) {
|
||||
this._container = container;
|
||||
container.classList.add('monaco-panel-view');
|
||||
|
||||
this._splitView = new SplitView(container);
|
||||
|
||||
const itemsHeaderTitle = nls.localize("insights.dialog.items", "Items");
|
||||
const itemsDetailHeaderTitle = nls.localize("insights.dialog.itemDetails", "Item Details");
|
||||
|
||||
this._topTableData = new TableDataView();
|
||||
this._bottomTableData = new TableDataView();
|
||||
let topTableView = this._instantiationService.createInstance(InsightTableView, this._topColumns, this._topTableData, { forceFitColumns: true }, { id: 'insights.top', title: itemsHeaderTitle, ariaHeaderLabel: itemsHeaderTitle }) as InsightTableView<ListResource>;
|
||||
topTableView.render();
|
||||
attachPanelStyler(topTableView, this._themeService);
|
||||
this._topTable = topTableView.table;
|
||||
this._topTable.setSelectionModel(new RowSelectionModel<ListResource>());
|
||||
let bottomTableView = this._instantiationService.createInstance(InsightTableView, this._bottomColumns, this._bottomTableData, { forceFitColumns: true }, { id: 'insights.bottom', title: itemsDetailHeaderTitle, ariaHeaderLabel: itemsDetailHeaderTitle }) as InsightTableView<ListResource>;
|
||||
bottomTableView.render();
|
||||
attachPanelStyler(bottomTableView, this._themeService);
|
||||
this._bottomTable = bottomTableView.table;
|
||||
this._bottomTable.setSelectionModel(new RowSelectionModel<ListResource>());
|
||||
|
||||
this._register(this._topTable.onSelectedRowsChanged((e: DOMEvent, data: Slick.OnSelectedRowsChangedEventArgs<ListResource>) => {
|
||||
if (data.rows.length === 1) {
|
||||
let element = this._topTableData.getItem(data.rows[0]);
|
||||
let resourceArray: ListResource[] = [];
|
||||
for (let i = 0; i < this._model.columns.length; i++) {
|
||||
resourceArray.push({ label: this._model.columns[i], value: element.data[i], data: element.data });
|
||||
}
|
||||
|
||||
this._bottomTableData.clear();
|
||||
this._bottomTableData.push(resourceArray);
|
||||
if (bottomTableView.isExpanded()) {
|
||||
bottomTableView.setExpanded(false);
|
||||
bottomTableView.setExpanded(true);
|
||||
}
|
||||
this._enableTaskButtons(true);
|
||||
} else {
|
||||
this._enableTaskButtons(false);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._topTable.onContextMenu(e => {
|
||||
if (this.hasActions()) {
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => this.insightActions,
|
||||
getActionsContext: () => this.topInsightContext(this._topTableData.getItem(e.cell.row))
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._bottomTable.onContextMenu(e => {
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => TPromise.as([this._instantiationService.createInstance(CopyInsightDialogSelectionAction, CopyInsightDialogSelectionAction.ID, CopyInsightDialogSelectionAction.LABEL)]),
|
||||
getActionsContext: () => this.bottomInsightContext(this._bottomTableData.getItem(e.cell.row), e.cell)
|
||||
});
|
||||
}));
|
||||
|
||||
this._splitView.addView(topTableView, Sizing.Distribute);
|
||||
this._splitView.addView(bottomTableView, Sizing.Distribute);
|
||||
|
||||
this._register(attachTableStyler(this._topTable, this._themeService));
|
||||
this._register(attachTableStyler(this._bottomTable, this._themeService));
|
||||
|
||||
this._topTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
|
||||
topTableView.focus();
|
||||
e.stopImmediatePropagation();
|
||||
} else if (event.equals(KeyCode.Tab)) {
|
||||
bottomTableView.focus();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this._bottomTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
|
||||
bottomTableView.focus();
|
||||
e.stopImmediatePropagation();
|
||||
} else if (event.equals(KeyCode.Tab)) {
|
||||
let buttonFound = false;
|
||||
for (let index = 0; index < this._taskButtonDisposables.length; index++) {
|
||||
let element = this._taskButtonDisposables[index];
|
||||
if (element instanceof Button && element.enabled) {
|
||||
buttonFound = true;
|
||||
element.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!buttonFound) {
|
||||
this._closeButton.focus();
|
||||
}
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
super.render();
|
||||
this._closeButton = this.addFooterButton('Close', () => this.close());
|
||||
this._register(attachButtonStyler(this._closeButton, this._themeService));
|
||||
this._register(attachModalDialogStyler(this, this._themeService));
|
||||
}
|
||||
|
||||
protected layout(height?: number): void {
|
||||
this._splitView.layout(DOM.getContentHeight(this._container));
|
||||
}
|
||||
|
||||
// insight object
|
||||
public open(input: IInsightsConfigDetails, connectionProfile: IConnectionProfile): void {
|
||||
if (types.isUndefinedOrNull(input) || types.isUndefinedOrNull(connectionProfile)) {
|
||||
return;
|
||||
}
|
||||
this._insight = input;
|
||||
this._connectionProfile = connectionProfile;
|
||||
this.updateTopColumns();
|
||||
this.show();
|
||||
}
|
||||
|
||||
private build(): void {
|
||||
let labelIndex: number;
|
||||
let valueIndex: number;
|
||||
let columnName = this.labelColumnName;
|
||||
if (this._insight.label === undefined || (labelIndex = this._model.columns.indexOf(columnName)) === -1) {
|
||||
labelIndex = 0;
|
||||
}
|
||||
if (this._insight.value === undefined || (valueIndex = this._model.columns.indexOf(this._insight.value)) === -1) {
|
||||
valueIndex = 1;
|
||||
}
|
||||
// convert
|
||||
let inputArray = this._model.getListResources(labelIndex, valueIndex);
|
||||
this._topTableData.clear();
|
||||
this._topTableData.push(inputArray);
|
||||
if (this._insight.actions && this._insight.actions.types) {
|
||||
let tasks = TaskRegistry.getTasks();
|
||||
for (let action of this._insight.actions.types) {
|
||||
let task = tasks.includes(action);
|
||||
let commandAction = MenuRegistry.getCommand(action);
|
||||
let commandLabel = types.isString(commandAction.title) ? commandAction.title : commandAction.title.value;
|
||||
if (task && !this.findFooterButton(commandLabel)) {
|
||||
let button = this.addFooterButton(commandLabel, () => {
|
||||
let element = this._topTable.getSelectedRows();
|
||||
let resource: ListResource;
|
||||
if (element && element.length > 0) {
|
||||
resource = this._topTableData.getItem(element[0]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
let context = this.topInsightContext(resource);
|
||||
this._commandService.executeCommand(action, context);
|
||||
}, 'left');
|
||||
button.enabled = false;
|
||||
this._taskButtonDisposables.push(button);
|
||||
this._taskButtonDisposables.push(attachButtonStyler(button, this._themeService));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.layout();
|
||||
|
||||
// Select and focus the top row
|
||||
this._topTable.grid.gotoCell(0, 1);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._topTableData.clear();
|
||||
this._bottomTableData.clear();
|
||||
}
|
||||
|
||||
private get labelColumnName(): string {
|
||||
return typeof this._insight.label === 'object' ? this._insight.label.column : this._insight.label;
|
||||
}
|
||||
|
||||
|
||||
public close() {
|
||||
this.hide();
|
||||
dispose(this._taskButtonDisposables);
|
||||
this._taskButtonDisposables = [];
|
||||
}
|
||||
|
||||
protected onClose(e: StandardKeyboardEvent) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
private hasActions(): boolean {
|
||||
return !!(this._insight && this._insight.actions && this._insight.actions.types
|
||||
&& this._insight.actions.types.length > 0);
|
||||
}
|
||||
|
||||
private get insightActions(): TPromise<IAction[]> {
|
||||
let tasks = TaskRegistry.getTasks();
|
||||
let actions = this._insight.actions.types;
|
||||
let returnActions: IAction[] = [];
|
||||
for (let action of actions) {
|
||||
let task = tasks.includes(action);
|
||||
let commandAction = MenuRegistry.getCommand(action);
|
||||
if (task) {
|
||||
returnActions.push(this._instantiationService.createInstance(ExecuteCommandAction, commandAction.id, commandAction.title));
|
||||
}
|
||||
}
|
||||
return TPromise.as(returnActions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the context that should be passed to the action passed on the selected element for the top table
|
||||
* @param element
|
||||
*/
|
||||
private topInsightContext(element: ListResource): IConnectionProfile {
|
||||
let database = this._insight.actions.database || this._connectionProfile.databaseName;
|
||||
let server = this._insight.actions.server || this._connectionProfile.serverName;
|
||||
let user = this._insight.actions.user || this._connectionProfile.userName;
|
||||
let match: Array<string>;
|
||||
match = database.match(insertValueRegex);
|
||||
if (match && match.length > 0) {
|
||||
let index = this._model.columns.indexOf(match[1]);
|
||||
if (index === -1) {
|
||||
error('Could not find column', match[1]);
|
||||
} else {
|
||||
database = database.replace(match[0], element.data[index]);
|
||||
}
|
||||
}
|
||||
|
||||
match = server.match(insertValueRegex);
|
||||
if (match && match.length > 0) {
|
||||
let index = this._model.columns.indexOf(match[1]);
|
||||
if (index === -1) {
|
||||
error('Could not find column', match[1]);
|
||||
} else {
|
||||
server = server.replace(match[0], element.data[index]);
|
||||
}
|
||||
}
|
||||
|
||||
match = user.match(insertValueRegex);
|
||||
if (match && match.length > 0) {
|
||||
let index = this._model.columns.indexOf(match[1]);
|
||||
if (index === -1) {
|
||||
error('Could not find column', match[1]);
|
||||
} else {
|
||||
user = user.replace(match[0], element.data[index]);
|
||||
}
|
||||
}
|
||||
|
||||
let currentProfile = this._connectionProfile as ConnectionProfile;
|
||||
let profile = new ConnectionProfile(this._capabilitiesService, currentProfile);
|
||||
profile.databaseName = database;
|
||||
profile.serverName = server;
|
||||
profile.userName = user;
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the context that should be passed to the action passed on the selected element for the bottom table
|
||||
* @param element
|
||||
*/
|
||||
private bottomInsightContext(element: ListResource, cell: Slick.Cell): IInsightDialogActionContext {
|
||||
|
||||
let cellData = element[this._bottomColumns[cell.cell].id];
|
||||
|
||||
return { profile: undefined, cellData };
|
||||
}
|
||||
|
||||
private _enableTaskButtons(val: boolean): void {
|
||||
for (let index = 0; index < this._taskButtonDisposables.length; index++) {
|
||||
let element = this._taskButtonDisposables[index];
|
||||
if (element instanceof Button) {
|
||||
element.enabled = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.insights span {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.insights .icon-span {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.insights .badge .badge-content {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 8px;
|
||||
min-width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.insights .badge {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 5px;
|
||||
overflow: hidden;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsightDialogActionContext } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
|
||||
export class CopyInsightDialogSelectionAction extends Action {
|
||||
public static ID = 'workbench.action.insights.copySelection';
|
||||
public static LABEL = nls.localize('workbench.action.insights.copySelection', "Copy Cell");
|
||||
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IClipboardService private _clipboardService: IClipboardService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(event?: IInsightDialogActionContext): TPromise<any> {
|
||||
this._clipboardService.writeText(event.cellData);
|
||||
return TPromise.as(void 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsightsDialogModel, ListResource } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
import { IInsightsConfigDetails, IInsightsLabel } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { Conditional } from 'sql/parts/dashboard/common/interfaces';
|
||||
|
||||
import { Event, Emitter, debounceEvent } from 'vs/base/common/event';
|
||||
|
||||
export class InsightsDialogModel implements IInsightsDialogModel {
|
||||
private _rows: string[][];
|
||||
private _columns: string[];
|
||||
private _insight: IInsightsConfigDetails;
|
||||
|
||||
private _onDataChangeEmitter: Emitter<void> = new Emitter<void>();
|
||||
private _onDataChangeEvent: Event<void> = this._onDataChangeEmitter.event;
|
||||
public onDataChange: Event<void> = debounceEvent(this._onDataChangeEvent, (last, event) => event, 75, false);
|
||||
|
||||
public set insight(insight: IInsightsConfigDetails) {
|
||||
this._insight = insight;
|
||||
}
|
||||
|
||||
public set rows(val: string[][]) {
|
||||
this._rows = val;
|
||||
this._onDataChangeEmitter.fire();
|
||||
}
|
||||
|
||||
public get rows(): string[][] {
|
||||
return this._rows;
|
||||
}
|
||||
|
||||
public set columns(val: string[]) {
|
||||
this._columns = val;
|
||||
this._onDataChangeEmitter.fire();
|
||||
}
|
||||
|
||||
public get columns(): string[] {
|
||||
return this._columns;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._columns = [];
|
||||
this._rows = [];
|
||||
this._onDataChangeEmitter.fire();
|
||||
}
|
||||
|
||||
public getListResources(labelIndex: number, valueIndex: number): ListResource[] {
|
||||
return this.rows.map((item) => {
|
||||
let label = item[labelIndex];
|
||||
let value = item[valueIndex];
|
||||
let state = this.calcInsightState(value);
|
||||
let data = item;
|
||||
let icon = typeof this._insight.label === 'object' ? this._insight.label.icon : undefined;
|
||||
let rval = { title: false, label, value, icon, data };
|
||||
if (state) {
|
||||
rval[state.type] = state.val;
|
||||
}
|
||||
return rval;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the state of the item value passed based on the insight conditions
|
||||
* @param item item to determine state for
|
||||
* @returns json that specifies whether the state is an icon or color and the val of that state
|
||||
*/
|
||||
private calcInsightState(item: string): { type: 'stateColor' | 'stateIcon', val: string } {
|
||||
if (typeof this._insight.label === 'string') {
|
||||
return undefined;
|
||||
} else {
|
||||
let label = <IInsightsLabel>this._insight.label;
|
||||
for (let cond of label.state) {
|
||||
switch (Conditional[cond.condition.if]) {
|
||||
case Conditional.always:
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
case Conditional.equals:
|
||||
if (item === cond.condition.equals) {
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
}
|
||||
break;
|
||||
case Conditional.notEquals:
|
||||
if (item !== cond.condition.equals) {
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
}
|
||||
break;
|
||||
case Conditional.greaterThanOrEquals:
|
||||
if (parseInt(item) >= parseInt(cond.condition.equals)) {
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
}
|
||||
break;
|
||||
case Conditional.greaterThan:
|
||||
if (parseInt(item) > parseInt(cond.condition.equals)) {
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
}
|
||||
break;
|
||||
case Conditional.lessThanOrEquals:
|
||||
if (parseInt(item) <= parseInt(cond.condition.equals)) {
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
}
|
||||
break;
|
||||
case Conditional.lessThan:
|
||||
if (parseInt(item) < parseInt(cond.condition.equals)) {
|
||||
return cond.color
|
||||
? { type: 'stateColor', val: cond.color }
|
||||
: { type: 'stateIcon', val: cond.icon };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we got to this point, there was no matching conditionals therefore no valid state
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { IInsightsConfigDetails, IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { BaseActionContext } from 'sql/workbench/common/actions';
|
||||
|
||||
export interface IInsightsDialogModel {
|
||||
rows: string[][];
|
||||
columns: string[];
|
||||
getListResources(labelIndex: number, valueIndex: number): ListResource[];
|
||||
reset(): void;
|
||||
onDataChange: Event<void>;
|
||||
insight: IInsightsConfigDetails;
|
||||
}
|
||||
|
||||
export interface ListResource {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
data?: string[];
|
||||
stateColor?: string;
|
||||
stateIcon?: string;
|
||||
}
|
||||
|
||||
export const IInsightsDialogService = createDecorator<IInsightsDialogService>('insightsDialogService');
|
||||
|
||||
export interface IInsightsDialogService {
|
||||
_serviceBrand: any;
|
||||
show(input: IInsightsConfig, connectionProfile: IConnectionProfile): void;
|
||||
close();
|
||||
}
|
||||
|
||||
export interface IInsightDialogActionContext extends BaseActionContext {
|
||||
cellData: string;
|
||||
}
|
||||
|
||||
/* Regex that matches the form `${value}` */
|
||||
export const insertValueRegex: RegExp = /\${(.*?)\}/;
|
||||
@@ -0,0 +1,199 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IInsightsConfigDetails } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import QueryRunner, { EventType as QREvents } from 'sql/platform/query/common/queryRunner';
|
||||
import * as Utils from 'sql/platform/connection/common/utils';
|
||||
import { IInsightsDialogModel, insertValueRegex } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
|
||||
import { DbCellValue, IDbColumn, QueryExecuteSubsetResult } from 'sqlops';
|
||||
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export class InsightsDialogController {
|
||||
private _queryRunner: QueryRunner;
|
||||
private _connectionProfile: IConnectionProfile;
|
||||
private _connectionUri: string;
|
||||
private _columns: IDbColumn[];
|
||||
private _rows: DbCellValue[][];
|
||||
|
||||
constructor(
|
||||
private _model: IInsightsDialogModel,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IErrorMessageService private _errorMessageService: IErrorMessageService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IWorkspaceContextService private _workspaceContextService: IWorkspaceContextService
|
||||
) { }
|
||||
|
||||
public update(input: IInsightsConfigDetails, connectionProfile: IConnectionProfile): Thenable<void> {
|
||||
// execute string
|
||||
if (typeof input === 'object') {
|
||||
if (connectionProfile === undefined) {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize("insightsInputError", "No Connection Profile was passed to insights flyout")
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (types.isStringArray(input.query)) {
|
||||
return this.createQuery(input.query.join(' '), connectionProfile).catch(e => {
|
||||
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), e);
|
||||
}).then(() => undefined);
|
||||
} else if (types.isString(input.query)) {
|
||||
return this.createQuery(input.query, connectionProfile).catch(e => {
|
||||
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), e);
|
||||
}).then(() => undefined);
|
||||
} else if (types.isString(input.queryFile)) {
|
||||
let filePath = input.queryFile;
|
||||
// check for workspace relative path
|
||||
let match = filePath.match(insertValueRegex);
|
||||
if (match && match.length > 0 && match[1] === 'workspaceRoot') {
|
||||
filePath = filePath.replace(match[0], '');
|
||||
|
||||
switch (this._workspaceContextService.getWorkbenchState()) {
|
||||
case WorkbenchState.FOLDER:
|
||||
filePath = this._workspaceContextService.getWorkspace().folders[0].toResource(filePath).fsPath;
|
||||
break;
|
||||
case WorkbenchState.WORKSPACE:
|
||||
let filePathArray = filePath.split('/');
|
||||
// filter out empty sections
|
||||
filePathArray = filePathArray.filter(i => !!i);
|
||||
let folder = this._workspaceContextService.getWorkspace().folders.find(i => i.name === filePathArray[0]);
|
||||
if (!folder) {
|
||||
return Promise.reject<void>(new Error(`Could not find workspace folder ${filePathArray[0]}`));
|
||||
}
|
||||
// remove the folder name from the filepath
|
||||
filePathArray.shift();
|
||||
// rejoin the filepath after doing the work to find the right folder
|
||||
filePath = '/' + filePathArray.join('/');
|
||||
filePath = folder.toResource(filePath).fsPath;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
pfs.readFile(filePath).then(
|
||||
buffer => {
|
||||
this.createQuery(buffer.toString(), connectionProfile).catch(e => {
|
||||
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), e);
|
||||
}).then(() => resolve());
|
||||
},
|
||||
error => {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize("insightsFileError", "There was an error reading the query file: ") + error
|
||||
});
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
error('Error reading details Query: ', input);
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize("insightsConfigError", "There was an error parsing the insight config; could not find query array/string or queryfile")
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async createQuery(queryString: string, connectionProfile: IConnectionProfile): Promise<void> {
|
||||
if (this._queryRunner) {
|
||||
if (!this._queryRunner.hasCompleted) {
|
||||
await this._queryRunner.cancelQuery();
|
||||
}
|
||||
try {
|
||||
await this.createNewConnection(connectionProfile);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
this._queryRunner.uri = this._connectionUri;
|
||||
} else {
|
||||
try {
|
||||
await this.createNewConnection(connectionProfile);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
this._queryRunner = this._instantiationService.createInstance(QueryRunner, this._connectionUri);
|
||||
this.addQueryEventListeners(this._queryRunner);
|
||||
}
|
||||
|
||||
return this._queryRunner.runQuery(queryString);
|
||||
}
|
||||
|
||||
private async createNewConnection(connectionProfile: IConnectionProfile): Promise<void> {
|
||||
// determine if we need to create a new connection
|
||||
if (!this._connectionProfile || connectionProfile.getOptionsKey() !== this._connectionProfile.getOptionsKey()) {
|
||||
if (this._connectionProfile) {
|
||||
try {
|
||||
await this._connectionManagementService.disconnect(this._connectionUri);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
this._connectionProfile = connectionProfile;
|
||||
this._connectionUri = Utils.generateUri(this._connectionProfile, 'insights');
|
||||
return this._connectionManagementService.connect(this._connectionProfile, this._connectionUri).then(result => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private addQueryEventListeners(queryRunner: QueryRunner): void {
|
||||
queryRunner.addListener(QREvents.COMPLETE, () => {
|
||||
this.queryComplete().catch(error => {
|
||||
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), error);
|
||||
});
|
||||
});
|
||||
queryRunner.addListener(QREvents.MESSAGE, message => {
|
||||
if (message.isError) {
|
||||
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), message.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async queryComplete(): Promise<void> {
|
||||
let batches = this._queryRunner.batchSets;
|
||||
// currently only support 1 batch set 1 resultset
|
||||
if (batches.length > 0) {
|
||||
let batch = batches[0];
|
||||
if (batch.resultSetSummaries.length > 0
|
||||
&& batch.resultSetSummaries[0].rowCount > 0
|
||||
) {
|
||||
let resultset = batch.resultSetSummaries[0];
|
||||
this._columns = resultset.columnInfo;
|
||||
let rows: QueryExecuteSubsetResult;
|
||||
try {
|
||||
rows = await this._queryRunner.getQueryRows(0, resultset.rowCount, batch.id, resultset.id);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
this._rows = rows.resultSubset.rows;
|
||||
this.updateModel();
|
||||
}
|
||||
}
|
||||
// TODO issue #2746 should ideally show a warning inside the dialog if have no data
|
||||
}
|
||||
|
||||
private updateModel(): void {
|
||||
let data = this._rows.map(r => r.map(c => c.displayValue));
|
||||
let columns = this._columns.map(c => c.columnName);
|
||||
|
||||
this._model.rows = data;
|
||||
this._model.columns = columns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,771 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { NodeType } from 'sql/parts/objectExplorer/common/nodeType';
|
||||
import { TreeNode, TreeItemCollapsibleState } from 'sql/parts/objectExplorer/common/treeNode';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as TelemetryKeys from 'sql/common/telemetryKeys';
|
||||
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { warn, error } from 'sql/base/common/log';
|
||||
import { ServerTreeView } from 'sql/parts/objectExplorer/viewlet/serverTreeView';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import * as Utils from 'sql/platform/connection/common/utils';
|
||||
|
||||
export const SERVICE_ID = 'ObjectExplorerService';
|
||||
|
||||
export const IObjectExplorerService = createDecorator<IObjectExplorerService>(SERVICE_ID);
|
||||
|
||||
export interface NodeExpandInfoWithProviderId extends sqlops.ObjectExplorerExpandInfo {
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
export interface IObjectExplorerService {
|
||||
_serviceBrand: any;
|
||||
|
||||
createNewSession(providerId: string, connection: ConnectionProfile): Thenable<sqlops.ObjectExplorerSessionResponse>;
|
||||
|
||||
closeSession(providerId: string, session: sqlops.ObjectExplorerSession): Thenable<sqlops.ObjectExplorerCloseSessionResponse>;
|
||||
|
||||
expandNode(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string): Thenable<sqlops.ObjectExplorerExpandInfo>;
|
||||
|
||||
refreshNode(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string): Thenable<sqlops.ObjectExplorerExpandInfo>;
|
||||
|
||||
resolveTreeNodeChildren(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable<TreeNode[]>;
|
||||
|
||||
refreshTreeNode(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable<TreeNode[]>;
|
||||
|
||||
onSessionCreated(handle: number, sessionResponse: sqlops.ObjectExplorerSession);
|
||||
|
||||
onSessionDisconnected(handle: number, sessionResponse: sqlops.ObjectExplorerSession);
|
||||
|
||||
onNodeExpanded(sessionResponse: NodeExpandInfoWithProviderId);
|
||||
|
||||
/**
|
||||
* Register a ObjectExplorer provider
|
||||
*/
|
||||
registerProvider(providerId: string, provider: sqlops.ObjectExplorerProvider): void;
|
||||
|
||||
registerNodeProvider(expander: sqlops.ObjectExplorerNodeProvider): void;
|
||||
|
||||
getObjectExplorerNode(connection: IConnectionProfile): TreeNode;
|
||||
|
||||
updateObjectExplorerNodes(connectionProfile: IConnectionProfile): Promise<void>;
|
||||
|
||||
deleteObjectExplorerNode(connection: IConnectionProfile): Thenable<void>;
|
||||
|
||||
onUpdateObjectExplorerNodes: Event<ObjectExplorerNodeEventArgs>;
|
||||
|
||||
registerServerTreeView(view: ServerTreeView): void;
|
||||
|
||||
getSelectedProfileAndDatabase(): { profile: ConnectionProfile, databaseName: string };
|
||||
|
||||
isFocused(): boolean;
|
||||
|
||||
onSelectionOrFocusChange: Event<void>;
|
||||
|
||||
getServerTreeView(): ServerTreeView;
|
||||
|
||||
findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames?: string[]): Thenable<sqlops.NodeInfo[]>;
|
||||
|
||||
getActiveConnectionNodes(): TreeNode[];
|
||||
|
||||
getTreeNode(connectionId: string, nodePath: string): Thenable<TreeNode>;
|
||||
|
||||
refreshNodeInView(connectionId: string, nodePath: string): Thenable<TreeNode>;
|
||||
|
||||
/**
|
||||
* For Testing purpose only. Get the context menu actions for an object explorer node.
|
||||
*/
|
||||
getNodeActions(connectionId: string, nodePath: string): Thenable<string[]>;
|
||||
|
||||
getSessionConnectionProfile(sessionId: string): sqlops.IConnectionProfile;
|
||||
}
|
||||
|
||||
interface SessionStatus {
|
||||
nodes: { [nodePath: string]: NodeStatus };
|
||||
connection: ConnectionProfile;
|
||||
expandNodeTimer?: number;
|
||||
}
|
||||
|
||||
interface NodeStatus {
|
||||
expandEmitter: Emitter<NodeExpandInfoWithProviderId>;
|
||||
}
|
||||
|
||||
export interface ObjectExplorerNodeEventArgs {
|
||||
connection: IConnectionProfile;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export interface NodeInfoWithConnection {
|
||||
connectionId: string;
|
||||
nodeInfo: sqlops.NodeInfo;
|
||||
}
|
||||
|
||||
export interface TopLevelChildrenPath {
|
||||
providerId: string;
|
||||
supportedProviderId: string;
|
||||
groupingId: number;
|
||||
path: string[];
|
||||
providerObject: sqlops.ObjectExplorerNodeProvider | sqlops.ObjectExplorerProvider;
|
||||
}
|
||||
|
||||
export class ObjectExplorerService implements IObjectExplorerService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
private _providers: { [handle: string]: sqlops.ObjectExplorerProvider; } = Object.create(null);
|
||||
|
||||
private _nodeProviders: { [handle: string]: sqlops.ObjectExplorerNodeProvider[]; } = Object.create(null);
|
||||
|
||||
private _activeObjectExplorerNodes: { [id: string]: TreeNode };
|
||||
private _sessions: { [sessionId: string]: SessionStatus };
|
||||
|
||||
private _onUpdateObjectExplorerNodes: Emitter<ObjectExplorerNodeEventArgs>;
|
||||
|
||||
private _serverTreeView: ServerTreeView;
|
||||
|
||||
private _onSelectionOrFocusChange: Emitter<void>;
|
||||
|
||||
constructor(
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@ITelemetryService private _telemetryService: ITelemetryService,
|
||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService
|
||||
) {
|
||||
this._onUpdateObjectExplorerNodes = new Emitter<ObjectExplorerNodeEventArgs>();
|
||||
this._activeObjectExplorerNodes = {};
|
||||
this._sessions = {};
|
||||
this._providers = {};
|
||||
this._nodeProviders = {};
|
||||
this._onSelectionOrFocusChange = new Emitter<void>();
|
||||
}
|
||||
|
||||
public get onUpdateObjectExplorerNodes(): Event<ObjectExplorerNodeEventArgs> {
|
||||
return this._onUpdateObjectExplorerNodes.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when the selection or focus of Object Explorer changes
|
||||
*/
|
||||
public get onSelectionOrFocusChange(): Event<void> {
|
||||
return this._onSelectionOrFocusChange.event;
|
||||
}
|
||||
|
||||
public updateObjectExplorerNodes(connection: IConnectionProfile): Promise<void> {
|
||||
return this._connectionManagementService.addSavedPassword(connection).then(withPassword => {
|
||||
let connectionProfile = ConnectionProfile.fromIConnectionProfile(this._capabilitiesService, withPassword);
|
||||
return this.updateNewObjectExplorerNode(connectionProfile);
|
||||
});
|
||||
}
|
||||
|
||||
public deleteObjectExplorerNode(connection: IConnectionProfile): Thenable<void> {
|
||||
let self = this;
|
||||
var connectionUri = connection.id;
|
||||
var nodeTree = this._activeObjectExplorerNodes[connectionUri];
|
||||
if (nodeTree) {
|
||||
return self.closeSession(connection.providerName, nodeTree.getSession()).then(() => {
|
||||
delete self._activeObjectExplorerNodes[connectionUri];
|
||||
delete self._sessions[nodeTree.getSession().sessionId];
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when expanded node response is ready
|
||||
*/
|
||||
public onNodeExpanded(expandResponse: NodeExpandInfoWithProviderId) {
|
||||
|
||||
if (expandResponse.errorMessage) {
|
||||
error(expandResponse.errorMessage);
|
||||
}
|
||||
|
||||
let sessionStatus = this._sessions[expandResponse.sessionId];
|
||||
let foundSession = false;
|
||||
if (sessionStatus) {
|
||||
let nodeStatus = this._sessions[expandResponse.sessionId].nodes[expandResponse.nodePath];
|
||||
foundSession = !!nodeStatus;
|
||||
if (foundSession && nodeStatus.expandEmitter) {
|
||||
nodeStatus.expandEmitter.fire(expandResponse);
|
||||
}
|
||||
}
|
||||
if (!foundSession) {
|
||||
warn(`Cannot find node status for session: ${expandResponse.sessionId} and node path: ${expandResponse.nodePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when session is created
|
||||
*/
|
||||
public onSessionCreated(handle: number, session: sqlops.ObjectExplorerSession): void {
|
||||
this.handleSessionCreated(session);
|
||||
}
|
||||
|
||||
private async handleSessionCreated(session: sqlops.ObjectExplorerSession): Promise<void> {
|
||||
try {
|
||||
let connection: ConnectionProfile = undefined;
|
||||
let errorMessage: string = undefined;
|
||||
if (this._sessions[session.sessionId]) {
|
||||
connection = this._sessions[session.sessionId].connection;
|
||||
|
||||
if (session && session.success && session.rootNode) {
|
||||
let server = this.toTreeNode(session.rootNode, null);
|
||||
server.connection = connection;
|
||||
server.session = session;
|
||||
this._activeObjectExplorerNodes[connection.id] = server;
|
||||
}
|
||||
else {
|
||||
errorMessage = session && session.errorMessage ? session.errorMessage :
|
||||
nls.localize('OeSessionFailedError', 'Failed to create Object Explorer session');
|
||||
error(errorMessage);
|
||||
}
|
||||
// Send on session created about the session to all node providers so they can prepare for node expansion
|
||||
let nodeProviders = this._nodeProviders[connection.providerName];
|
||||
if (nodeProviders) {
|
||||
let promises: Thenable<boolean>[] = nodeProviders.map(p => p.handleSessionOpen(session));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
else {
|
||||
warn(`cannot find session ${session.sessionId}`);
|
||||
}
|
||||
|
||||
this.sendUpdateNodeEvent(connection, errorMessage);
|
||||
} catch (error) {
|
||||
warn(`cannot handle the session ${session.sessionId} in all nodeProviders`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when session is disconnected
|
||||
*/
|
||||
public onSessionDisconnected(handle: number, session: sqlops.ObjectExplorerSession) {
|
||||
if (this._sessions[session.sessionId]) {
|
||||
let connection: ConnectionProfile = this._sessions[session.sessionId].connection;
|
||||
if (connection && this._connectionManagementService.isProfileConnected(connection)) {
|
||||
let uri: string = Utils.generateUri(connection);
|
||||
if (this._serverTreeView.isObjectExplorerConnectionUri(uri)) {
|
||||
this._serverTreeView.deleteObjectExplorerNodeAndRefreshTree(connection).then(() => {
|
||||
this.sendUpdateNodeEvent(connection, session.errorMessage);
|
||||
connection.isDisconnecting = true;
|
||||
this._connectionManagementService.disconnect(connection).then((value) => {
|
||||
connection.isDisconnecting = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn(`Cannot find session ${session.sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
private sendUpdateNodeEvent(connection: ConnectionProfile, errorMessage: string = undefined) {
|
||||
let eventArgs: ObjectExplorerNodeEventArgs = {
|
||||
connection: <IConnectionProfile>connection,
|
||||
errorMessage: errorMessage
|
||||
};
|
||||
this._onUpdateObjectExplorerNodes.fire(eventArgs);
|
||||
}
|
||||
|
||||
private updateNewObjectExplorerNode(connection: ConnectionProfile): Promise<void> {
|
||||
let self = this;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (self._activeObjectExplorerNodes[connection.id]) {
|
||||
this.sendUpdateNodeEvent(connection);
|
||||
resolve();
|
||||
} else {
|
||||
// Create session will send the event or reject the promise
|
||||
this.createNewSession(connection.providerName, connection).then(response => {
|
||||
resolve();
|
||||
}, error => {
|
||||
this.sendUpdateNodeEvent(connection, error);
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getObjectExplorerNode(connection: IConnectionProfile): TreeNode {
|
||||
return this._activeObjectExplorerNodes[connection.id];
|
||||
}
|
||||
|
||||
public async createNewSession(providerId: string, connection: ConnectionProfile): Promise<sqlops.ObjectExplorerSessionResponse> {
|
||||
let self = this;
|
||||
return new Promise<sqlops.ObjectExplorerSessionResponse>((resolve, reject) => {
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
provider.createNewSession(connection.toConnectionInfo()).then(result => {
|
||||
self._sessions[result.sessionId] = {
|
||||
connection: connection,
|
||||
nodes: {}
|
||||
};
|
||||
resolve(result);
|
||||
}, error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
reject(`Provider doesn't exist. id: ${providerId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public expandNode(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string): Thenable<sqlops.ObjectExplorerExpandInfo> {
|
||||
return new Promise<sqlops.ObjectExplorerExpandInfo>((resolve, reject) => {
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ObjectExplorerExpand, { refresh: 0, provider: providerId });
|
||||
this.expandOrRefreshNode(providerId, session, nodePath).then(result => {
|
||||
resolve(result);
|
||||
}, error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
reject(`Provider doesn't exist. id: ${providerId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private callExpandOrRefreshFromProvider(provider: sqlops.ObjectExplorerProviderBase, nodeInfo: sqlops.ExpandNodeInfo, refresh: boolean = false) {
|
||||
if (refresh) {
|
||||
return provider.refreshNode(nodeInfo);
|
||||
} else {
|
||||
return provider.expandNode(nodeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private expandOrRefreshNode(
|
||||
providerId: string,
|
||||
session: sqlops.ObjectExplorerSession,
|
||||
nodePath: string,
|
||||
refresh: boolean = false): Thenable<sqlops.ObjectExplorerExpandInfo> {
|
||||
let self = this;
|
||||
return new Promise<sqlops.ObjectExplorerExpandInfo>((resolve, reject) => {
|
||||
if (session.sessionId in self._sessions && self._sessions[session.sessionId]) {
|
||||
let newRequest = false;
|
||||
if (!self._sessions[session.sessionId].nodes[nodePath]) {
|
||||
self._sessions[session.sessionId].nodes[nodePath] = {
|
||||
expandEmitter: new Emitter<NodeExpandInfoWithProviderId>()
|
||||
};
|
||||
newRequest = true;
|
||||
}
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
let resultMap: Map<string, sqlops.ObjectExplorerExpandInfo> = new Map<string, sqlops.ObjectExplorerExpandInfo>();
|
||||
let allProviders: sqlops.ObjectExplorerProviderBase[] = [provider];
|
||||
|
||||
let nodeProviders = this._nodeProviders[providerId];
|
||||
if (nodeProviders) {
|
||||
nodeProviders = nodeProviders.sort((a, b) => a.group.toLowerCase().localeCompare(b.group.toLowerCase()));
|
||||
allProviders.push(...nodeProviders);
|
||||
}
|
||||
|
||||
self._sessions[session.sessionId].nodes[nodePath].expandEmitter.event((expandResult) => {
|
||||
if (expandResult && expandResult.providerId) {
|
||||
resultMap.set(expandResult.providerId, expandResult);
|
||||
} else {
|
||||
error('OE provider returns empty result or providerId');
|
||||
}
|
||||
|
||||
// When get all responses from all providers, merge results
|
||||
if (resultMap.size === allProviders.length) {
|
||||
resolve(self.mergeResults(allProviders, resultMap, nodePath));
|
||||
|
||||
// Have to delete it after get all reponses otherwise couldn't find session for not the first response
|
||||
if (newRequest) {
|
||||
delete self._sessions[session.sessionId].nodes[nodePath];
|
||||
}
|
||||
}
|
||||
});
|
||||
if (newRequest) {
|
||||
allProviders.forEach(provider => {
|
||||
self.callExpandOrRefreshFromProvider(provider, {
|
||||
sessionId: session.sessionId,
|
||||
nodePath: nodePath
|
||||
}, refresh).then(isExpanding => {
|
||||
if (!isExpanding) {
|
||||
// The provider stated it's not going to expand the node, therefore do not need to track when merging results
|
||||
let emptyResult: sqlops.ObjectExplorerExpandInfo = {
|
||||
errorMessage: undefined,
|
||||
nodePath: nodePath,
|
||||
nodes: [],
|
||||
sessionId: session.sessionId
|
||||
};
|
||||
resultMap.set(provider.providerId, emptyResult);
|
||||
}
|
||||
}, error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reject(`session cannot find to expand node. id: ${session.sessionId} nodePath: ${nodePath}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private mergeResults(allProviders: sqlops.ObjectExplorerProviderBase[], resultMap: Map<string, sqlops.ObjectExplorerExpandInfo>, nodePath: string): sqlops.ObjectExplorerExpandInfo {
|
||||
let finalResult: sqlops.ObjectExplorerExpandInfo;
|
||||
let allNodes: sqlops.NodeInfo[] = [];
|
||||
let errorNode: sqlops.NodeInfo = {
|
||||
nodePath: nodePath,
|
||||
label: 'Error',
|
||||
errorMessage: '',
|
||||
nodeType: 'error',
|
||||
isLeaf: true,
|
||||
nodeSubType: '',
|
||||
nodeStatus: '',
|
||||
metadata: null
|
||||
};
|
||||
let errorMessages: string[] = [];
|
||||
for (let provider of allProviders) {
|
||||
if (resultMap.has(provider.providerId)) {
|
||||
let result = resultMap.get(provider.providerId);
|
||||
if (result) {
|
||||
if (!result.errorMessage) {
|
||||
finalResult = result;
|
||||
if (result.nodes !== undefined && result.nodes) {
|
||||
allNodes = allNodes.concat(result.nodes);
|
||||
}
|
||||
} else {
|
||||
errorMessages.push(result.errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finalResult) {
|
||||
if (errorMessages.length > 0) {
|
||||
if (errorMessages.length > 1) {
|
||||
errorMessages.unshift(nls.localize('nodeExpansionError', 'Mulitiple errors:'));
|
||||
}
|
||||
errorNode.errorMessage = errorMessages.join('\n');
|
||||
errorNode.label = errorNode.errorMessage;
|
||||
allNodes = [errorNode].concat(allNodes);
|
||||
}
|
||||
|
||||
finalResult.nodes = allNodes;
|
||||
}
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
public refreshNode(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string): Thenable<sqlops.ObjectExplorerExpandInfo> {
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ObjectExplorerExpand, { refresh: 1, provider: providerId });
|
||||
return this.expandOrRefreshNode(providerId, session, nodePath, true);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public closeSession(providerId: string, session: sqlops.ObjectExplorerSession): Thenable<sqlops.ObjectExplorerCloseSessionResponse> {
|
||||
// Complete any requests that are still open for the session
|
||||
let sessionStatus = this._sessions[session.sessionId];
|
||||
if (sessionStatus && sessionStatus.nodes) {
|
||||
Object.entries(sessionStatus.nodes).forEach(([nodePath, nodeStatus]: [string, NodeStatus]) => {
|
||||
if (nodeStatus.expandEmitter) {
|
||||
nodeStatus.expandEmitter.fire({
|
||||
sessionId: session.sessionId,
|
||||
nodes: [],
|
||||
nodePath: nodePath,
|
||||
errorMessage: undefined,
|
||||
providerId: providerId
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let provider = this._providers[providerId];
|
||||
if (provider) {
|
||||
let nodeProviders = this._nodeProviders[providerId];
|
||||
if (nodeProviders) {
|
||||
for (let nodeProvider of nodeProviders) {
|
||||
nodeProvider.handleSessionClose({
|
||||
sessionId: session ? session.sessionId : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
return provider.closeSession({
|
||||
sessionId: session ? session.sessionId : undefined
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a ObjectExplorer provider
|
||||
*/
|
||||
public registerProvider(providerId: string, provider: sqlops.ObjectExplorerProvider): void {
|
||||
this._providers[providerId] = provider;
|
||||
}
|
||||
|
||||
public registerNodeProvider(nodeProvider: sqlops.ObjectExplorerNodeProvider): void {
|
||||
let nodeProviders = this._nodeProviders[nodeProvider.supportedProviderId] || [];
|
||||
nodeProviders.push(nodeProvider);
|
||||
this._nodeProviders[nodeProvider.supportedProviderId] = nodeProviders;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposables = dispose(this._disposables);
|
||||
}
|
||||
|
||||
public resolveTreeNodeChildren(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable<TreeNode[]> {
|
||||
// Always refresh the node if it has an error, otherwise expand it normally
|
||||
let needsRefresh = !!parentTree.errorStateMessage;
|
||||
return this.expandOrRefreshTreeNode(session, parentTree, needsRefresh);
|
||||
}
|
||||
|
||||
public refreshTreeNode(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable<TreeNode[]> {
|
||||
return this.expandOrRefreshTreeNode(session, parentTree, true);
|
||||
}
|
||||
|
||||
private callExpandOrRefreshFromService(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string, refresh: boolean = false): Thenable<sqlops.ObjectExplorerExpandInfo> {
|
||||
if (refresh) {
|
||||
return this.refreshNode(providerId, session, nodePath);
|
||||
} else {
|
||||
return this.expandNode(providerId, session, nodePath);
|
||||
}
|
||||
}
|
||||
|
||||
private expandOrRefreshTreeNode(
|
||||
session: sqlops.ObjectExplorerSession,
|
||||
parentTree: TreeNode,
|
||||
refresh: boolean = false): Thenable<TreeNode[]> {
|
||||
return new Promise<TreeNode[]>((resolve, reject) => {
|
||||
this.callExpandOrRefreshFromService(parentTree.getConnectionProfile().providerName, session, parentTree.nodePath, refresh).then(expandResult => {
|
||||
let children: TreeNode[] = [];
|
||||
if (expandResult && expandResult.nodes) {
|
||||
children = expandResult.nodes.map(node => {
|
||||
return this.toTreeNode(node, parentTree);
|
||||
});
|
||||
parentTree.children = children.filter(c => c !== undefined);
|
||||
resolve(children);
|
||||
} else {
|
||||
reject(expandResult && expandResult.errorMessage ? expandResult.errorMessage : 'Failed to expand node');
|
||||
}
|
||||
}, error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private toTreeNode(nodeInfo: sqlops.NodeInfo, parent: TreeNode): TreeNode {
|
||||
// Show the status for database nodes with a status field
|
||||
let isLeaf: boolean = nodeInfo.isLeaf;
|
||||
if (nodeInfo.nodeType === NodeType.Database) {
|
||||
if (nodeInfo.nodeStatus) {
|
||||
nodeInfo.label = nodeInfo.label + ' (' + nodeInfo.nodeStatus + ')';
|
||||
}
|
||||
if (isLeaf) {
|
||||
// set to common status so we can have a single 'Unavailable' db icon
|
||||
nodeInfo.nodeStatus = 'Unavailable';
|
||||
} else {
|
||||
nodeInfo.nodeStatus = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return new TreeNode(nodeInfo.nodeType, nodeInfo.label, isLeaf, nodeInfo.nodePath,
|
||||
nodeInfo.nodeSubType, nodeInfo.nodeStatus, parent, nodeInfo.metadata, nodeInfo.iconType, {
|
||||
getChildren: treeNode => this.getChildren(treeNode),
|
||||
isExpanded: treeNode => this.isExpanded(treeNode),
|
||||
setNodeExpandedState: (treeNode, expandedState) => this.setNodeExpandedState(treeNode, expandedState),
|
||||
setNodeSelected: (treeNode, selected, clearOtherSelections: boolean = undefined) => this.setNodeSelected(treeNode, selected, clearOtherSelections)
|
||||
});
|
||||
}
|
||||
|
||||
public registerServerTreeView(view: ServerTreeView): void {
|
||||
if (this._serverTreeView) {
|
||||
throw new Error('The object explorer server tree view is already registered');
|
||||
}
|
||||
this._serverTreeView = view;
|
||||
this._serverTreeView.onSelectionOrFocusChange(() => this._onSelectionOrFocusChange.fire());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection profile corresponding to the current Object Explorer selection,
|
||||
* or undefined if there are multiple selections or no such connection
|
||||
*/
|
||||
public getSelectedProfileAndDatabase(): { profile: ConnectionProfile, databaseName: string } {
|
||||
if (!this._serverTreeView) {
|
||||
return undefined;
|
||||
}
|
||||
let selection = this._serverTreeView.getSelection();
|
||||
if (selection.length === 1) {
|
||||
let selectedNode = selection[0];
|
||||
if (selectedNode instanceof ConnectionProfile) {
|
||||
return { profile: selectedNode, databaseName: undefined };
|
||||
} else if (selectedNode instanceof TreeNode) {
|
||||
let profile = selectedNode.getConnectionProfile();
|
||||
let database = selectedNode.getDatabaseName();
|
||||
// If the database is unavailable, use the server connection
|
||||
if (selectedNode.nodeTypeId === 'Database' && selectedNode.isAlwaysLeaf) {
|
||||
database = undefined;
|
||||
}
|
||||
return { profile: profile, databaseName: database };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether the Object Explorer tree has focus
|
||||
*/
|
||||
public isFocused(): boolean {
|
||||
return this._serverTreeView.isFocused();
|
||||
}
|
||||
|
||||
public getServerTreeView() {
|
||||
return this._serverTreeView;
|
||||
}
|
||||
|
||||
public findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames?: string[]): Thenable<sqlops.NodeInfo[]> {
|
||||
let rootNode = this._activeObjectExplorerNodes[connectionId];
|
||||
if (!rootNode) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let sessionId = rootNode.session.sessionId;
|
||||
return this._providers[this._sessions[sessionId].connection.providerName].findNodes({
|
||||
type: type,
|
||||
name: name,
|
||||
schema: schema,
|
||||
database: database,
|
||||
parentObjectNames: parentObjectNames,
|
||||
sessionId: sessionId
|
||||
}).then(response => {
|
||||
return response.nodes;
|
||||
});
|
||||
}
|
||||
|
||||
public getActiveConnectionNodes(): TreeNode[] {
|
||||
return Object.values(this._activeObjectExplorerNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* For Testing purpose only. Get the context menu actions for an object explorer node
|
||||
*/
|
||||
public getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
|
||||
return this.getTreeNode(connectionId, nodePath).then(node => {
|
||||
return this._serverTreeView.treeActionProvider.getActions(this._serverTreeView.tree, this.getTreeItem(node)).then((actions) => {
|
||||
return actions.filter(action => action.label).map(action => action.label);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshNodeInView(connectionId: string, nodePath: string): Promise<TreeNode> {
|
||||
// Get the tree node and call refresh from the provider
|
||||
let treeNode = await this.getTreeNode(connectionId, nodePath);
|
||||
await this.refreshTreeNode(treeNode.getSession(), treeNode);
|
||||
|
||||
// Get the new tree node, refresh it in the view, and expand it if needed
|
||||
treeNode = await this.getTreeNode(connectionId, nodePath);
|
||||
await this._serverTreeView.refreshElement(treeNode);
|
||||
if (treeNode.children.length > 0) {
|
||||
await treeNode.setExpandedState(TreeItemCollapsibleState.Expanded);
|
||||
}
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
public getSessionConnectionProfile(sessionId: string): sqlops.IConnectionProfile {
|
||||
return this._sessions[sessionId].connection.toIConnectionProfile();
|
||||
}
|
||||
|
||||
private async setNodeExpandedState(treeNode: TreeNode, expandedState: TreeItemCollapsibleState): Promise<void> {
|
||||
treeNode = await this.getUpdatedTreeNode(treeNode);
|
||||
let expandNode = this.getTreeItem(treeNode);
|
||||
if (expandedState === TreeItemCollapsibleState.Expanded) {
|
||||
await this._serverTreeView.reveal(expandNode);
|
||||
}
|
||||
return this._serverTreeView.setExpandedState(expandNode, expandedState);
|
||||
}
|
||||
|
||||
private async setNodeSelected(treeNode: TreeNode, selected: boolean, clearOtherSelections: boolean = undefined): Promise<void> {
|
||||
treeNode = await this.getUpdatedTreeNode(treeNode);
|
||||
let selectNode = this.getTreeItem(treeNode);
|
||||
if (selected) {
|
||||
await this._serverTreeView.reveal(selectNode);
|
||||
}
|
||||
return this._serverTreeView.setSelected(selectNode, selected, clearOtherSelections);
|
||||
}
|
||||
|
||||
private async getChildren(treeNode: TreeNode): Promise<TreeNode[]> {
|
||||
treeNode = await this.getUpdatedTreeNode(treeNode);
|
||||
if (treeNode.isAlwaysLeaf) {
|
||||
return [];
|
||||
}
|
||||
if (!treeNode.children) {
|
||||
await this.resolveTreeNodeChildren(treeNode.getSession(), treeNode);
|
||||
}
|
||||
return treeNode.children;
|
||||
}
|
||||
|
||||
private async isExpanded(treeNode: TreeNode): Promise<boolean> {
|
||||
treeNode = await this.getUpdatedTreeNode(treeNode);
|
||||
do {
|
||||
let expandNode = this.getTreeItem(treeNode);
|
||||
if (!this._serverTreeView.isExpanded(expandNode)) {
|
||||
return false;
|
||||
}
|
||||
treeNode = treeNode.parent;
|
||||
} while (treeNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private getTreeItem(treeNode: TreeNode): TreeNode | ConnectionProfile {
|
||||
let rootNode = this._activeObjectExplorerNodes[treeNode.getConnectionProfile().id];
|
||||
if (treeNode === rootNode) {
|
||||
return treeNode.connection;
|
||||
}
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
private getUpdatedTreeNode(treeNode: TreeNode): Promise<TreeNode> {
|
||||
return this.getTreeNode(treeNode.getConnectionProfile().id, treeNode.nodePath).then(treeNode => {
|
||||
if (!treeNode) {
|
||||
throw new Error(nls.localize('treeNodeNoLongerExists', 'The given tree node no longer exists'));
|
||||
}
|
||||
return treeNode;
|
||||
});
|
||||
}
|
||||
|
||||
public async getTreeNode(connectionId: string, nodePath: string): Promise<TreeNode> {
|
||||
let parentNode = this._activeObjectExplorerNodes[connectionId];
|
||||
if (!parentNode) {
|
||||
return undefined;
|
||||
}
|
||||
if (!nodePath) {
|
||||
return parentNode;
|
||||
}
|
||||
let currentNode = parentNode;
|
||||
while (currentNode.nodePath !== nodePath) {
|
||||
let nextNode = undefined;
|
||||
if (!currentNode.isAlwaysLeaf && !currentNode.children) {
|
||||
await this.resolveTreeNodeChildren(currentNode.getSession(), currentNode);
|
||||
}
|
||||
if (currentNode.children) {
|
||||
// Look at the next node in the path, which is the child object with the longest path where the desired path starts with the child path
|
||||
let children = currentNode.children.filter(child => nodePath.startsWith(child.nodePath));
|
||||
if (children.length > 0) {
|
||||
nextNode = children.reduce((currentMax, candidate) => currentMax.nodePath.length < candidate.nodePath.length ? candidate : currentMax);
|
||||
}
|
||||
}
|
||||
if (!nextNode) {
|
||||
return undefined;
|
||||
}
|
||||
currentNode = nextNode;
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { EditDataInput } from 'sql/parts/editData/common/editDataInput';
|
||||
import { IConnectableInput, IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { IQueryEditorService, IQueryEditorOptions } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
||||
import { sqlModeId, untitledFilePrefix, getSupportedInputResource } from 'sql/parts/common/customInputConverter';
|
||||
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||
|
||||
import { IMode } from 'vs/editor/common/modes';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import nls = require('vs/nls');
|
||||
import URI from 'vs/base/common/uri';
|
||||
import paths = require('vs/base/common/paths');
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { EditDataResultsInput } from 'sql/parts/editData/common/editDataResultsInput';
|
||||
import { IEditorInput, IEditor } from 'vs/workbench/common/editor';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Service wrapper for opening and creating SQL documents as sql editor inputs
|
||||
*/
|
||||
export class QueryEditorService implements IQueryEditorService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
|
||||
private static CHANGE_UNSUPPORTED_ERROR_MESSAGE = nls.localize(
|
||||
'queryEditorServiceChangeUnsupportedError',
|
||||
'Change Language Mode is not supported for unsaved queries'
|
||||
);
|
||||
|
||||
private static CHANGE_ERROR_MESSAGE = nls.localize(
|
||||
'queryEditorServiceChangeError',
|
||||
'Please save or discard changes before switching to/from the SQL Language Mode'
|
||||
);
|
||||
|
||||
// service references for static functions
|
||||
private static editorService: IEditorService;
|
||||
private static instantiationService: IInstantiationService;
|
||||
private static notificationService: INotificationService;
|
||||
|
||||
constructor(
|
||||
@INotificationService _notificationService: INotificationService,
|
||||
@IUntitledEditorService private _untitledEditorService: IUntitledEditorService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IEditorService private _editorService: IEditorService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService
|
||||
) {
|
||||
QueryEditorService.editorService = _editorService;
|
||||
QueryEditorService.instantiationService = _instantiationService;
|
||||
QueryEditorService.notificationService = _notificationService;
|
||||
}
|
||||
|
||||
////// Public functions
|
||||
|
||||
/**
|
||||
* Creates new untitled document for SQL query and opens in new editor tab
|
||||
*/
|
||||
public newSqlEditor(sqlContent?: string, connectionProviderName?: string, isDirty?: boolean): Promise<IConnectableInput> {
|
||||
return new Promise<IConnectableInput>((resolve, reject) => {
|
||||
try {
|
||||
// Create file path and file URI
|
||||
let filePath = this.createUntitledSqlFilePath();
|
||||
let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath });
|
||||
|
||||
// Create a sql document pane with accoutrements
|
||||
const fileInput = this._untitledEditorService.createOrGet(docUri, 'sql');
|
||||
fileInput.resolve().then(m => {
|
||||
if (sqlContent) {
|
||||
m.textEditorModel.setValue(sqlContent);
|
||||
if (isDirty === false || (isDirty === undefined && !this._configurationService.getValue<boolean>('sql.promptToSaveGeneratedFiles'))) {
|
||||
m.setDirty(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const queryResultsInput: QueryResultsInput = this._instantiationService.createInstance(QueryResultsInput, docUri.toString());
|
||||
let queryInput: QueryInput = this._instantiationService.createInstance(QueryInput, '', fileInput, queryResultsInput, connectionProviderName);
|
||||
|
||||
this._editorService.openEditor(queryInput, { pinned: true })
|
||||
.then((editor) => {
|
||||
let params = <QueryInput>editor.input;
|
||||
resolve(params);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a new query plan document
|
||||
public newQueryPlanEditor(xmlShowPlan: string): Promise<any> {
|
||||
const self = this;
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
let queryPlanInput: QueryPlanInput = self._instantiationService.createInstance(QueryPlanInput, xmlShowPlan, 'aaa', undefined);
|
||||
self._editorService.openEditor(queryPlanInput, { pinned: true }, ACTIVE_GROUP);
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new edit data session
|
||||
*/
|
||||
public newEditDataEditor(schemaName: string, tableName: string, sqlContent: string): Promise<IConnectableInput> {
|
||||
|
||||
return new Promise<IConnectableInput>((resolve, reject) => {
|
||||
try {
|
||||
// Create file path and file URI
|
||||
let objectName = schemaName ? schemaName + '.' + tableName : tableName;
|
||||
let filePath = this.createPrefixedSqlFilePath(objectName);
|
||||
let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath });
|
||||
|
||||
// Create a sql document pane with accoutrements
|
||||
const fileInput = this._untitledEditorService.createOrGet(docUri, 'sql');
|
||||
fileInput.resolve().then(m => {
|
||||
if (sqlContent) {
|
||||
m.textEditorModel.setValue(sqlContent);
|
||||
}
|
||||
});
|
||||
|
||||
// Create an EditDataInput for editing
|
||||
const resultsInput: EditDataResultsInput = this._instantiationService.createInstance(EditDataResultsInput, docUri.toString());
|
||||
let editDataInput: EditDataInput = this._instantiationService.createInstance(EditDataInput, docUri, schemaName, tableName, fileInput, sqlContent, resultsInput);
|
||||
|
||||
this._editorService.openEditor(editDataInput, { pinned: true })
|
||||
.then((editor) => {
|
||||
let params = <EditDataInput>editor.input;
|
||||
resolve(params);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSaveAsCompleted(oldResource: URI, newResource: URI): void {
|
||||
let oldResourceString: string = oldResource.toString();
|
||||
|
||||
|
||||
this._editorService.editors.forEach(input => {
|
||||
if (input instanceof QueryInput) {
|
||||
const resource = input.getResource();
|
||||
|
||||
// Update Editor if file (or any parent of the input) got renamed or moved
|
||||
// Note: must check the new file name for this since this method is called after the rename is completed
|
||||
if (paths.isEqualOrParent(resource.fsPath, newResource.fsPath, !isLinux /* ignorecase */)) {
|
||||
// In this case, we know that this is a straight rename so support this as a rename / replace operation
|
||||
TaskUtilities.replaceConnection(oldResourceString, newResource.toString(), this._connectionManagementService).then(result => {
|
||||
if (result && result.connected) {
|
||||
input.onConnectSuccess();
|
||||
} else {
|
||||
input.onConnectReject();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
////// Public static functions
|
||||
// These functions are static to reduce extra lines needed in the vscode code base
|
||||
|
||||
/**
|
||||
* Checks if the Language Mode is being changed to/from SQL. If so, swaps out the input of the
|
||||
* given editor with a new input, opens a new editor, then returns the new editor's IModel.
|
||||
*
|
||||
* Returns an immediately resolved promise if the SQL Language mode is not involved. In this case,
|
||||
* the calling function in editorStatus.ts will handle the language change normally.
|
||||
*
|
||||
* Returns an immediately resolved promise with undefined if SQL is involved in the language change
|
||||
* and the editor is dirty. In this case, the calling function in editorStatus.ts will not perform
|
||||
* the language change. TODO: change this - tracked by issue #727
|
||||
*
|
||||
* In all other cases (when SQL is involved in the language change and the editor is not dirty),
|
||||
* returns a promise that will resolve when the old editor has been replaced by a new editor.
|
||||
*/
|
||||
public static sqlLanguageModeCheck(model: ITextModel, mode: IMode, editor: IEditor): Promise<ITextModel> {
|
||||
if (!model || !mode || !editor) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
let newLanguage: string = mode.getLanguageIdentifier().language;
|
||||
let oldLanguage: string = model.getLanguageIdentifier().language;
|
||||
let changingToSql = sqlModeId === newLanguage;
|
||||
let changingFromSql = sqlModeId === oldLanguage;
|
||||
let changingLanguage = newLanguage !== oldLanguage;
|
||||
|
||||
if (!changingLanguage) {
|
||||
return Promise.resolve(model);
|
||||
}
|
||||
if (!changingFromSql && !changingToSql) {
|
||||
return Promise.resolve(model);
|
||||
}
|
||||
|
||||
let uri: URI = QueryEditorService._getEditorChangeUri(editor.input, changingToSql);
|
||||
if (uri.scheme === Schemas.untitled && (editor.input instanceof QueryInput || editor.input instanceof EditDataInput)) {
|
||||
QueryEditorService.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: QueryEditorService.CHANGE_UNSUPPORTED_ERROR_MESSAGE
|
||||
});
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Return undefined to notify the calling funciton to not perform the language change
|
||||
// TODO change this - tracked by issue #727
|
||||
if (editor.input.isDirty()) {
|
||||
QueryEditorService.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: QueryEditorService.CHANGE_ERROR_MESSAGE
|
||||
});
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
let group: IEditorGroup = editor.group;
|
||||
let index: number = group.editors.indexOf(editor.input);
|
||||
let options: IQueryEditorOptions = editor.options ? editor.options : {};
|
||||
options = Object.assign(options, { index: index });
|
||||
|
||||
// Return a promise that will resovle when the old editor has been replaced by a new editor
|
||||
return new Promise<ITextModel>((resolve, reject) => {
|
||||
let newEditorInput = QueryEditorService._getNewEditorInput(changingToSql, editor.input, uri);
|
||||
|
||||
// Override queryEditorCheck to not open this file in a QueryEditor
|
||||
if (!changingToSql) {
|
||||
options.denyQueryEditor = true;
|
||||
}
|
||||
|
||||
group.closeEditor(editor.input).then(() => {
|
||||
// Reopen a new editor in the same position/index
|
||||
QueryEditorService.editorService.openEditor(newEditorInput, options, group).then((editor) => {
|
||||
resolve(QueryEditorService._onEditorOpened(editor, uri.toString(), undefined, options.pinned));
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
////// Private functions
|
||||
|
||||
private createUntitledSqlFilePath(): string {
|
||||
return this.createPrefixedSqlFilePath(untitledFilePrefix);
|
||||
}
|
||||
|
||||
private createPrefixedSqlFilePath(prefix: string): string {
|
||||
let prefixFileName = (counter: number): string => {
|
||||
return `${prefix}_${counter}`;
|
||||
};
|
||||
|
||||
let counter = 1;
|
||||
// Get document name and check if it exists
|
||||
let filePath = prefixFileName(counter);
|
||||
while (fs.existsSync(filePath)) {
|
||||
counter++;
|
||||
filePath = prefixFileName(counter);
|
||||
}
|
||||
|
||||
let untitledEditors = this._untitledEditorService.getAll();
|
||||
while (untitledEditors.find(x => x.getName().toUpperCase() === filePath.toUpperCase())) {
|
||||
counter++;
|
||||
filePath = prefixFileName(counter);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
////// Private static functions
|
||||
|
||||
/**
|
||||
* Returns a QueryInput if we are changingToSql. Returns a FileEditorInput if we are !changingToSql.
|
||||
*/
|
||||
private static _getNewEditorInput(changingToSql: boolean, input: IEditorInput, uri: URI): IEditorInput {
|
||||
if (!uri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let newEditorInput: IEditorInput = undefined;
|
||||
if (changingToSql) {
|
||||
const queryResultsInput: QueryResultsInput = QueryEditorService.instantiationService.createInstance(QueryResultsInput, uri.toString());
|
||||
let queryInput: QueryInput = QueryEditorService.instantiationService.createInstance(QueryInput, '', input, queryResultsInput, undefined);
|
||||
newEditorInput = queryInput;
|
||||
} else {
|
||||
let uriCopy: URI = URI.from({ scheme: uri.scheme, authority: uri.authority, path: uri.path, query: uri.query, fragment: uri.fragment });
|
||||
newEditorInput = QueryEditorService.instantiationService.createInstance(FileEditorInput, uriCopy, undefined);
|
||||
}
|
||||
|
||||
return newEditorInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URI for this IEditorInput or returns undefined if one does not exist.
|
||||
*/
|
||||
private static _getEditorChangeUri(input: IEditorInput, changingToSql: boolean): URI {
|
||||
let uriSource: IEditorInput = input;
|
||||
|
||||
// It is assumed that if we got here, !changingToSql is logically equivalent to changingFromSql
|
||||
let changingFromSql = !changingToSql;
|
||||
if (input instanceof QueryInput && changingFromSql) {
|
||||
let queryInput: QueryInput = <QueryInput>input;
|
||||
uriSource = queryInput.sql;
|
||||
}
|
||||
return getSupportedInputResource(uriSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle all cleanup actions that need to wait until the editor is fully open.
|
||||
*/
|
||||
private static _onEditorOpened(editor: IEditor, uri: string, position: Position, isPinned: boolean): ITextModel {
|
||||
|
||||
// Reset the editor pin state
|
||||
// TODO: change this so it happens automatically in openEditor in sqlLanguageModeCheck. Performing this here
|
||||
// causes the text on the tab to slightly flicker for unpinned files (from non-italic to italic to non-italic).
|
||||
// This is currently unavoidable because vscode ignores "pinned" on IEditorOptions if "index" is not undefined,
|
||||
// and we need to specify "index"" so the editor tab remains in the same place
|
||||
// let group: IEditorGroup = QueryEditorService.editorGroupService.getStacksModel().groupAt(position);
|
||||
// if (isPinned) {
|
||||
// QueryEditorService.editorGroupService.pinEditor(group, editor.input);
|
||||
// }
|
||||
|
||||
// @SQLTODO do we need the below
|
||||
// else {
|
||||
// QueryEditorService.editorGroupService.p .unpinEditor(group, editor.input);
|
||||
// }
|
||||
|
||||
// Grab and returns the IModel that will be used to resolve the sqlLanguageModeCheck promise.
|
||||
let control = editor.getControl();
|
||||
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
||||
let newModel = codeEditor ? codeEditor.getModel() : undefined;
|
||||
return newModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EditorInput } from 'vs/workbench/common/editor';
|
||||
import { IEditorDescriptor, IEditorRegistry, Extensions } from 'vs/workbench/browser/editor';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IEditorDescriptorService {
|
||||
_serviceBrand: any;
|
||||
|
||||
getEditor(input: EditorInput): IEditorDescriptor;
|
||||
}
|
||||
|
||||
export class EditorDescriptorService implements IEditorDescriptorService {
|
||||
public _serviceBrand: any;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public getEditor(input: EditorInput): IEditorDescriptor {
|
||||
return Registry.as<IEditorRegistry>(Extensions.Editors).getEditor(input);
|
||||
}
|
||||
}
|
||||
|
||||
export const SERVICE_ID = 'editorDescriptorService';
|
||||
|
||||
export const IEditorDescriptorService = createDecorator<IEditorDescriptorService>(SERVICE_ID);
|
||||
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IConnectableInput } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
|
||||
import URI from 'vs/base/common/uri';
|
||||
|
||||
export interface IQueryEditorOptions extends IEditorOptions {
|
||||
|
||||
// Tells IQueryEditorService.queryEditorCheck to not open this input in the QueryEditor.
|
||||
// Used when the user changes the Language Mode to not-SQL for files with .sql extensions.
|
||||
denyQueryEditor?: boolean;
|
||||
}
|
||||
|
||||
export const IQueryEditorService = createDecorator<IQueryEditorService>('QueryEditorService');
|
||||
|
||||
export interface IQueryEditorService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
// Creates new untitled document for SQL queries and opens it in a new editor tab
|
||||
newSqlEditor(sqlContent?: string, connectionProviderName?: string, isDirty?: boolean): Promise<IConnectableInput>;
|
||||
|
||||
// Creates a new query plan document
|
||||
newQueryPlanEditor(xmlShowPlan: string): Promise<any>;
|
||||
|
||||
// Creates new edit data session
|
||||
newEditDataEditor(schemaName: string, tableName: string, queryString: string): Promise<IConnectableInput>;
|
||||
|
||||
/**
|
||||
* Handles updating of SQL files on a save as event. These need special consideration
|
||||
* due to query results and other information being tied to the URI of the file
|
||||
* @param {URI} oldResource URI of the file before the save as was completed
|
||||
* @param {URI} newResource URI of the file after the save as operation was completed
|
||||
* @memberof IQueryEditorService
|
||||
*/
|
||||
onSaveAsCompleted(oldResource: URI, newResource: URI): void;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Write telemetry into a file for test purposes
|
||||
*/
|
||||
export class FileTelemetryService implements ITelemetryService {
|
||||
_serviceBrand: undefined;
|
||||
private _isFirst = true;
|
||||
|
||||
constructor(private _outputFile: string) {
|
||||
}
|
||||
|
||||
publicLog(eventName: string, data?: ITelemetryData) {
|
||||
let telemetryData = JSON.stringify(Object.assign({ eventName: eventName, data: data }));
|
||||
if (this._outputFile) {
|
||||
if (this._isFirst) {
|
||||
fs.open(this._outputFile, fs.O_WRONLY | fs.O_CREAT, (err, fr) => {
|
||||
fs.writeFileSync(this._outputFile, telemetryData + '\n');
|
||||
this._isFirst = false;
|
||||
});
|
||||
} else {
|
||||
fs.appendFileSync(this._outputFile, telemetryData + '\n');
|
||||
}
|
||||
}
|
||||
return TPromise.wrap<void>(null);
|
||||
}
|
||||
isOptedIn: true;
|
||||
getTelemetryInfo(): TPromise<ITelemetryInfo> {
|
||||
return TPromise.wrap({
|
||||
instanceId: 'someValue.instanceId',
|
||||
sessionId: 'someValue.sessionId',
|
||||
machineId: 'someValue.machineId'
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user