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:
Anthony Dresser
2019-02-19 12:11:54 -08:00
committed by GitHub
parent 4a82abc19b
commit d4704e39ac
162 changed files with 382 additions and 371 deletions

View File

@@ -3,14 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { WebViewDialog } from 'sql/base/browser/ui/modal/webViewDialog';
import 'vs/css!sql/media/icons/common-icons';
import { WebViewDialog } from 'sql/workbench/parts/webview/electron-browser/webViewDialog';
import { MainThreadModalDialogShape, SqlMainContext, SqlExtHostContext, ExtHostModalDialogsShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import 'vs/css!sql/media/icons/common-icons';
@extHostNamedCustomer(SqlMainContext.MainThreadModalDialog)
export class MainThreadModalDialog implements MainThreadModalDialogShape {
@@ -19,7 +19,6 @@ export class MainThreadModalDialog implements MainThreadModalDialogShape {
constructor(
context: IExtHostContext,
@IEditorService private readonly _editorService: IEditorService,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
this._proxy = context.getProxy(SqlExtHostContext.ExtHostModalDialogs);

View File

@@ -8,8 +8,8 @@ import { SqlExtHostContext, SqlMainContext, ExtHostConnectionManagementShape, Ma
import * as sqlops from 'sqlops';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IConnectionManagementService, IConnectionDialogService } from 'sql/platform/connection/common/connectionManagement';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
@@ -18,6 +18,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types';
import { generateUuid } from 'vs/base/common/uuid';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
@extHostNamedCustomer(SqlMainContext.MainThreadConnectionManagement)
export class MainThreadConnectionManagement implements MainThreadConnectionManagementShape {

View File

@@ -15,7 +15,7 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import * as sqlops from 'sqlops';
import { IMetadataService } from 'sql/platform/metadata/common/metadataService';
import { IObjectExplorerService, NodeExpandInfoWithProviderId } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IObjectExplorerService, NodeExpandInfoWithProviderId } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
import { IAdminService } from 'sql/workbench/services/admin/common/adminService';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';

View File

@@ -10,7 +10,7 @@ import * as vscode from 'vscode';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IObjectExplorerService, NodeInfoWithConnection } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IObjectExplorerService, NodeInfoWithConnection } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';

View File

@@ -8,7 +8,7 @@ import { SqlExtHostContext, SqlMainContext, ExtHostQueryEditorShape, MainThreadQ
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Button } from 'sql/base/browser/ui/button/button';
import { Builder } from 'vs/base/browser/builder';
import * as types from 'vs/base/common/types';
import * as sqlops from 'sqlops';
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string, rowContainerClass?: string): Builder {
let cellContainer: Builder;
let rowAttributes = rowContainerClass ? { class: rowContainerClass } : {};
container.element('tr', rowAttributes, (rowContainer) => {
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
labelCellContainer.div({}, (labelContainer) => {
labelContainer.text(label);
});
});
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
cellContainer = inputCellContainer;
});
});
return cellContainer;
}
export function appendRowLink(container: Builder, label: string, labelClass: string, cellContainerClass: string): Builder {
let rowButton: Button;
container.element('tr', {}, (rowContainer) => {
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
labelCellContainer.div({}, (labelContainer) => {
labelContainer.text(label);
});
});
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
inputCellContainer.element('div', {}, (rowContainer) => {
rowButton = new Button(rowContainer);
});
});
});
return new Builder(rowButton.element);
}
export function appendInputSelectBox(container: Builder, selectBox: SelectBox): SelectBox {
selectBox.render(container.getHTMLElement());
return selectBox;
}
export function getBooleanValueFromStringOrBoolean(value: any): boolean {
if (types.isBoolean(value)) {
return value;
} else if (types.isString(value)) {
return value.toLowerCase() === 'true';
}
return false;
}
export function getCategoryDisplayName(categories: sqlops.CategoryValue[], categoryName: string) {
var displayName: string;
categories.forEach(c => {
if (c.name === categoryName) {
displayName = c.displayName;
}
});
return displayName;
}
export function getCategoryName(categories: sqlops.CategoryValue[], categoryDisplayName: string) {
var categoryName: string;
categories.forEach(c => {
if (c.displayName === categoryDisplayName) {
categoryName = c.name;
}
});
return categoryName;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>back_16x16</title><path class="cls-1" d="M16.15,8.5H2.1l6.15,6.15-.7.7L.19,8,7.55.65l.7.7L2.1,7.5h14Z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>back_inverse_16x16</title><path class="cls-1" d="M16.15,8.5H2.1l6.15,6.15-.7.7L.19,8,7.55.65l.7.7L2.1,7.5h14Z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>close</title><path d="M8.82,8.1,15,14.29l-.81.81L8,8.91,1.81,15.1,1,14.29,7.18,8.1,1,1.9l.81-.81L8,7.28,14.19,1.1,15,1.9Z"/></svg>

After

Width:  |  Height:  |  Size: 230 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>close copy</title><path class="cls-1" d="M8.82,8.1,15,14.29l-.81.81L8,8.91,1.81,15.1,1,14.29,7.18,8.1,1,1.9l.81-.81L8,7.28,14.19,1.1,15,1.9Z"/></svg>

After

Width:  |  Height:  |  Size: 295 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>copy</title><path d="M3-.15V3.21H0V15.95H13V12.58h3V-.15ZM12,15H1.08V4.12H12Zm3-3.36H13V3.21h-9V.75H15Z"/></svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>copy_inverse</title><path class="cls-1" d="M3,0V3.36H0V16.1H13V12.73h3V0Zm9,15.19H1.08V4.27H12Zm3-3.36H13V3.36H4V.9H15Z"/></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>error_notification</title><path d="M8,0a7.92,7.92,0,0,1,4,1.09A8.15,8.15,0,0,1,14.91,4a8,8,0,0,1,.81,1.91,8,8,0,0,1-.81,6.16A8.15,8.15,0,0,1,12,14.92a8,8,0,0,1-8.07,0,8.15,8.15,0,0,1-2.87-2.87A8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.11,7.92,7.92,0,0,1,8,0ZM8,15a6.88,6.88,0,0,0,1.86-.25,7,7,0,0,0,4.89-4.89,7.07,7.07,0,0,0,0-3.73A7,7,0,0,0,9.86,1.27a7.07,7.07,0,0,0-3.73,0A7,7,0,0,0,1.25,6.15a7.07,7.07,0,0,0,0,3.73,7,7,0,0,0,4.89,4.89A6.88,6.88,0,0,0,8,15Zm3.46-9.76L8.71,8l2.75,2.76-.7.7L8,8.73,5.24,11.48l-.7-.7L7.29,8,4.54,5.26l.7-.7L8,7.31l2.76-2.75Z"/></svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>error_notification_inverse</title><path class="cls-1" d="M8,0a7.92,7.92,0,0,1,4,1.09A8.15,8.15,0,0,1,14.91,4a8,8,0,0,1,.81,1.91,8,8,0,0,1-.81,6.16A8.15,8.15,0,0,1,12,14.92a8,8,0,0,1-8.07,0,8.15,8.15,0,0,1-2.87-2.87A8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.11,7.92,7.92,0,0,1,8,0ZM8,15a6.88,6.88,0,0,0,1.86-.25,7,7,0,0,0,4.89-4.89,7.07,7.07,0,0,0,0-3.73A7,7,0,0,0,9.86,1.27a7.07,7.07,0,0,0-3.73,0A7,7,0,0,0,1.25,6.15a7.07,7.07,0,0,0,0,3.73,7,7,0,0,0,4.89,4.89A6.88,6.88,0,0,0,8,15Zm3.46-9.76L8.71,8l2.75,2.76-.7.7L8,8.73,5.24,11.48l-.7-.7L7.29,8,4.54,5.26l.7-.7L8,7.31l2.76-2.75Z"/></svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>info_notification</title><path d="M8,16a7.93,7.93,0,0,1-2.13-.28A8,8,0,0,1,4,14.91,8,8,0,0,1,0,8,7.94,7.94,0,0,1,.28,5.88,8,8,0,0,1,1.09,4,8,8,0,0,1,4,1.1,8.08,8.08,0,0,1,5.86.3a8.13,8.13,0,0,1,4.25,0,8,8,0,0,1,3.53,2.05A8,8,0,0,1,14.9,4a8,8,0,0,1,.8,1.91,8,8,0,0,1-2.05,7.78A7.93,7.93,0,0,1,12,14.91a8,8,0,0,1-1.91.8A7.93,7.93,0,0,1,8,16ZM8,1.08a6.72,6.72,0,0,0-1.84.25A7.09,7.09,0,0,0,4.5,2,6.92,6.92,0,0,0,2,4.52a7.09,7.09,0,0,0-.7,1.66,6.87,6.87,0,0,0,0,3.67A7.07,7.07,0,0,0,2,11.5,6.92,6.92,0,0,0,4.5,14a7.09,7.09,0,0,0,1.65.7,6.89,6.89,0,0,0,3.68,0,7.07,7.07,0,0,0,1.66-.7A6.92,6.92,0,0,0,14,11.5a7.08,7.08,0,0,0,.7-1.65,6.89,6.89,0,0,0,0-3.68A7.09,7.09,0,0,0,14,4.52,6.92,6.92,0,0,0,11.48,2a7.07,7.07,0,0,0-1.66-.7A6.74,6.74,0,0,0,8,1.08Zm-.53,3.2H8.52V5.34H7.46Zm0,2.13H8.52v5.33H7.46Z"/></svg>

After

Width:  |  Height:  |  Size: 902 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>info_notification_inverse</title><path class="cls-1" d="M8,16a7.93,7.93,0,0,1-2.13-.28A8,8,0,0,1,4,14.91,8,8,0,0,1,0,8,7.94,7.94,0,0,1,.28,5.88,8,8,0,0,1,1.09,4,8,8,0,0,1,4,1.1,8.08,8.08,0,0,1,5.86.3a8.13,8.13,0,0,1,4.25,0,8,8,0,0,1,3.53,2.05A8,8,0,0,1,14.9,4a8,8,0,0,1,.8,1.91,8,8,0,0,1-2.05,7.78A7.93,7.93,0,0,1,12,14.91a8,8,0,0,1-1.91.8A7.93,7.93,0,0,1,8,16ZM8,1.08a6.72,6.72,0,0,0-1.84.25A7.09,7.09,0,0,0,4.5,2,6.92,6.92,0,0,0,2,4.52a7.09,7.09,0,0,0-.7,1.66,6.87,6.87,0,0,0,0,3.67A7.07,7.07,0,0,0,2,11.5,6.92,6.92,0,0,0,4.5,14a7.09,7.09,0,0,0,1.65.7,6.89,6.89,0,0,0,3.68,0,7.07,7.07,0,0,0,1.66-.7A6.92,6.92,0,0,0,14,11.5a7.08,7.08,0,0,0,.7-1.65,6.89,6.89,0,0,0,0-3.68A7.09,7.09,0,0,0,14,4.52,6.92,6.92,0,0,0,11.48,2a7.07,7.07,0,0,0-1.66-.7A6.74,6.74,0,0,0,8,1.08Zm-.53,3.2H8.52V5.34H7.46Zm0,2.13H8.52v5.33H7.46Z"/></svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@@ -0,0 +1,285 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-shell .modal {
background-color: rgba(204, 204, 204, 0.6);
right: 0;
left: 0;
top: 0;
bottom: 0;
position: absolute;
z-index: 500;
}
.monaco-shell .modal:not(.flyout-dialog) .modal-dialog {
margin: auto;
width: 640px;
height: 480px;
}
.modal .modal-header {
padding: 15px;
}
.modal .modal-footer {
padding: 15px;
}
.modal .icon.in-progress {
width: 25px;
height: 25px;
}
.monaco-shell .modal.flyout-dialog .modal-dialog {
margin: auto auto auto auto;
height: 100%;
width: 500px;
right: 0;
position: absolute;
overflow-y: hidden;
}
.monaco-shell .modal.flyout-dialog.wide .modal-dialog {
width: 1200px;
max-width: 95%;
min-width: 400px;
}
.monaco-shell .modal.flyout-dialog .modal-content {
height: 100%;
font-size: 11px;
display: flex;
flex-direction: column;
}
.modal .modal-title {
font-size: 15px;
font-weight: 600;
}
.modal .modal-title-icon {
width: 20px;
height: 20px;
float: left;
margin-right: 10px;
}
.monaco-shell .modal.flyout-dialog .modal-body,
.monaco-shell .modal.flyout-dialog .angular-modal-body,
/* Style for body and footer in modal dialog. This should be applied to dialog created with angular component. */
.monaco-shell .modal.flyout-dialog .modal-body-and-footer {
flex: 1 1;
overflow: hidden;
}
/* modl body content style(excluding dialogErrorMessage section) for angulr component dialog */
.angular-modal-body-content {
overflow-x: hidden;
overflow-y: auto;
padding: 15px;
}
.modal.flyout-dialog .angular-form {
height: 100%;
display: flex;
flex-direction: column;
}
.modal.flyout-dialog .dialog-label {
width: 100%;
padding-bottom: 5px;
}
/* Add space in the bottom to separate each input elements */
.modal.flyout-dialog .input-divider {
width: 100%;
padding-bottom: 20px;
}
.modal.flyout-dialog .input {
width: 100%;
border: none;
height: 25px;
padding-left: 4px;
}
.vs-dark.monaco-shell .modal.flyout-dialog .input {
background-color: #3C3C3C;
}
.vs-dark.monaco-shell .modal.flyout-dialog input:disabled {
background-color: #E1E1E1;
color: #3C3C3C;
}
.modal .select-box,
.modal .monaco-select-box {
width: 100%;
height: 25px;
color: #6C6C6C;
font-size: 11px;
border: 1px solid transparent;
}
.modal .modal-footer {
display: flex;
}
.modal .modal-footer .left-footer {
display: flex;
flex: 1 1 auto;
justify-content: flex-start;
}
.modal .modal-footer .right-footer {
display: flex;
flex: 1 1 auto;
justify-content: flex-end;
}
.modal .footer-button a.monaco-button.monaco-text-button {
width: 100px;
}
.vs .monaco-text-button:focus {
outline-width: 1px;
}
.modal .footer-button {
margin-left: 5px;
}
.modal .right-footer .footer-button:last-of-type {
margin-right: none;
}
.modal.flyout-dialog .dialog-message {
padding: 6px 10px 10px 10px;
font-size: 13px;
display: flex;
flex-direction: column;
}
.vs .modal.flyout-dialog .dialog-message.error,
.vs-dark .modal.flyout-dialog .dialog-message.error {
background-color:#B62E00 !important;
color:#FFFFFF !important;
}
.vs .modal.flyout-dialog .dialog-message.warning,
.vs-dark .modal.flyout-dialog .dialog-message.warning {
background-color:#F9E385 !important;
color:#4A4A4A !important;
}
.vs .modal.flyout-dialog .dialog-message.info,
.vs-dark .modal.flyout-dialog .dialog-message.info {
background-color:#0078D7 !important;
color:#FFFFFF !important;
}
.modal.flyout-dialog .dialog-message-header {
overflow: hidden;
display: flex;
flex-direction: row;
}
.modal.flyout-dialog .dialog-message-body {
margin-left: 26px;
overflow-y: scroll;
overflow-x: hidden;
max-height: 90px;
display: flex;
flex-direction: column;
}
.modal.flyout-dialog .dialog-message-icon {
margin-right: 10px;
margin-top: 5px;
width: 16px;
height: 16px;
min-width: 16px;
}
.modal.flyout-dialog .dialog-message-severity {
margin-right: auto;
line-height: 26px;
}
.modal.flyout-dialog .dialog-message-button {
min-width: 60px;
margin-right: 10px;
}
.modal.flyout-dialog .dialog-message-button > a {
background-position-x: 2px !important;
background-color: inherit !important;
color:inherit !important;
padding-left: 22px !important;
background-size: 16px 16px !important;
text-align: left !important;
}
.modal.flyout-dialog .dialog-message-summary {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal.flyout-dialog .dialog-message-summary.expanded {
white-space: normal;
overflow-x: unset;
}
.modal.flyout-dialog .dialog-message-detail {
margin-top: 5px;
white-space: pre-wrap;
user-select: text;
font-size: 11px;
}
.hc-black .dialog-message.warning .close-message-icon,
.close-message-icon {
background: url('close_inverse.svg') center center no-repeat;
}
.dialog-message.warning .close-message-icon {
background: url('close.svg') center center no-repeat;
}
.hc-black .dialog-message.warning .copy-message-icon,
.copy-message-icon {
background: url('copy_inverse.svg') center center no-repeat;
}
.dialog-message.warning .copy-message-icon {
background: url('copy.svg') center center no-repeat;
}
.hc-black .dialog-message.warning .message-details-icon,
.message-details-icon {
background: url('show_details_inverse.svg') center center no-repeat;
}
.dialog-message.warning .message-details-icon {
background: url('show_details.svg') center center no-repeat;
}
.dialog-message.info .dialog-message-icon {
background: url('info_notification_inverse.svg') center center no-repeat;
}
.hc-black .dialog-message.warning .dialog-message-icon {
background: url('warning_notification_inverse.svg') center center no-repeat;
}
.dialog-message.warning .dialog-message-icon {
background: url('warning_notification.svg') center center no-repeat;
}
.dialog-message.error .dialog-message-icon {
background: url('error_notification_inverse.svg') center center no-repeat;
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.optionsDialog-label {
width: 120px;
padding-bottom: 5px;
}
.optionsDialog-input {
width: 200px;
padding-bottom: 5px;
padding-right: 7px;
}
.options-dialog .select-box {
width: 321px;
margin-left: 1px;
}
.optionsDialog-options-groups {
margin-top: 10px;
flex: 1 1;
overflow-y: auto;
}
.optionsDialog-options-groups {
margin: 10px 0px;
flex: 1 1;
overflow-y: auto;
}
.vs .optionsDialog-options-groups {
box-shadow: 0 1px 4px 1px rgba(220, 220, 220, 0.71);
}
.vs-dark .optionsDialog-options-groups {
box-shadow: 0 4px 5px 0px rgba(0, 0, 0, 0.71);
}
.optionsDialog-options-groups .split-view-view .header {
padding-left: 28px !important;
background-position-x: 8px !important;
}
.optionsDialog-options-groups .split-view-view .body {
padding-left: 5px !important;
}
.backButtonIcon {
content: url('back.svg');
width: 20px;
margin-right: 10px;
float: left;
cursor: pointer;
}
.hc-black .backButtonIcon,
.vs-dark .backButtonIcon {
content: url('back_inverse.svg');
}
.optionsDialog-description {
height: 90px;
margin: 15px;
overflow-y: auto;
}
.optionsDialog-description .modal-title {
background-repeat: no-repeat;
background-position: 2px center;
padding-left: 25px;
background-size: 16px;
}
.vs .optionsDialog-description .modal-title {
background-image: url('info_notification.svg')
}
.vs-dark .optionsDialog-description .modal-title,
.hc-black .optionsDialog-description .modal-title {
background-image: url('info_notification_inverse.svg')
}
.optionsDialog-options {
height: 100%;
display: flex;
flex-direction: column;
}
.optionsDialog-description-content {
padding-top: 10px;
padding-left: 25px;
}
.optionsDialog-table{
width:100%;
padding: 10px;
}
td.optionsDialog-input.select-container {
padding-right: 9px;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>show_details</title><path d="M1,0H15V16H1ZM14,15V1H2V15ZM13,3V4H3V3Zm0,2V6H3V5Zm0,2V8H3V7Zm0,2v1H3V9Zm0,2v1H3V11Z"/></svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>show_details_inverse</title><path class="cls-1" d="M1,0H15V16H1ZM14,15V1H2V15ZM13,3V4H3V3Zm0,2V6H3V5Zm0,2V8H3V7Zm0,2v1H3V9Zm0,2v1H3V11Z"/></svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>warning_notification</title><path d="M8,0l8,16H0ZM8,2.4,1.73,15H14.28Zm-.53,4H8.53v5.33H7.47Zm0,7.47V12.82H8.53v1.07Z"/></svg>

After

Width:  |  Height:  |  Size: 226 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>warning_notification_inverse</title><path class="cls-1" d="M8,0l8,16H0ZM8,2.4,1.73,15H14.28Zm-.53,4H8.53v5.33H7.47Zm0,7.47V12.82H8.53v1.07Z"/></svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,643 @@
/*---------------------------------------------------------------------------------------------
* 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/modal';
import { IThemable, attachButtonStyler } from 'vs/platform/theme/common/styler';
import { Color } from 'vs/base/common/color';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { mixin } from 'vs/base/common/objects';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Builder, $ } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { Button } from 'sql/base/browser/ui/button/button';
import * as TelemetryUtils from 'sql/common/telemetryUtilities';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { localize } from 'vs/nls';
import { MessageLevel } from 'sql/workbench/api/common/sqlExtHostTypes';
import * as os from 'os';
import { IThemeService } from 'vs/platform/theme/common/themeService';
export const MODAL_SHOWING_KEY = 'modalShowing';
export const MODAL_SHOWING_CONTEXT = new RawContextKey<Array<string>>(MODAL_SHOWING_KEY, []);
const INFO_ALT_TEXT = localize('infoAltText', 'Information');
const WARNING_ALT_TEXT = localize('warningAltText', 'Warning');
const ERROR_ALT_TEXT = localize('errorAltText', 'Error');
const SHOW_DETAILS_TEXT = localize('showMessageDetails', 'Show Details');
const HIDE_DETAILS_TEXT = localize('hideMessageDetails', 'Hide Details');
const COPY_TEXT = localize('copyMessage', 'Copy');
const CLOSE_TEXT = localize('closeMessage', 'Close');
const MESSAGE_EXPANDED_MODE_CLASS = 'expanded';
export interface IModalDialogStyles {
dialogForeground?: Color;
dialogBorder?: Color;
dialogHeaderAndFooterBackground?: Color;
dialogBodyBackground?: Color;
}
export interface IModalOptions {
isFlyout?: boolean;
isWide?: boolean;
isAngular?: boolean;
hasBackButton?: boolean;
hasTitleIcon?: boolean;
hasErrors?: boolean;
hasSpinner?: boolean;
}
// Needed for angular component dialogs to style modal footer
export class ModalFooterStyle {
public static backgroundColor;
public static borderTopWidth;
public static borderTopStyle;
public static borderTopColor;
}
const defaultOptions: IModalOptions = {
isFlyout: true,
isWide: false,
isAngular: false,
hasBackButton: false,
hasTitleIcon: false,
hasErrors: false,
hasSpinner: false
};
export abstract class Modal extends Disposable implements IThemable {
protected _useDefaultMessageBoxLocation: boolean = true;
protected _messageElement: HTMLElement;
private _messageIcon: HTMLElement;
private _messageSeverity: Builder;
private _messageSummary: Builder;
private _messageSummaryElement: HTMLElement;
private _messageDetail: Builder;
private _messageDetailElement: HTMLElement;
private _toggleMessageDetailButton: Button;
private _copyMessageButton: Button;
private _closeMessageButton: Button;
private _messageSummaryText: string;
private _messageDetailText: string;
private _spinnerElement: HTMLElement;
private _focusableElements: NodeListOf<Element>;
private _firstFocusableElement: HTMLElement;
private _lastFocusableElement: HTMLElement;
private _focusedElementBeforeOpen: HTMLElement;
private _dialogForeground: Color;
private _dialogBorder: Color;
private _dialogHeaderAndFooterBackground: Color;
private _dialogBodyBackground: Color;
private _modalDialog: Builder;
private _modalHeaderSection: Builder;
private _modalMessageSection: Builder;
private _modalBodySection: HTMLElement;
private _modalFooterSection: Builder;
private _closeButtonInHeader: Builder;
private _builder: Builder;
private _footerBuilder: Builder;
private _modalTitle: Builder;
private _modalTitleIcon: HTMLElement;
private _leftFooter: Builder;
private _rightFooter: Builder;
private _footerButtons: Button[];
private _keydownListener: IDisposable;
private _resizeListener: IDisposable;
private _modalOptions: IModalOptions;
private _backButton: Button;
private _modalShowingContext: IContextKey<Array<string>>;
private readonly _staticKey: string;
/**
* Get the back button, only available after render and if the hasBackButton option is true
*/
protected get backButton(): Button {
return this._backButton;
}
/**
* Set the dialog to have wide layout dynamically.
* Temporary solution to render file browser as wide or narrow layout.
* This will be removed once backup dialog is changed to wide layout.
* (hyoshi - 10/2/2017 tracked by https://github.com/Microsoft/carbon/issues/1836)
*/
public setWide(isWide: boolean): void {
if (this._builder.hasClass('wide') && isWide === false) {
this._builder.removeClass('wide');
} else if (!this._builder.hasClass('wide') && isWide === true) {
this._builder.addClass('wide');
}
}
/**
* Constructor for modal
* @param _title Title of the modal, if undefined, the title section is not rendered
* @param _name Name of the modal, used for telemetry
* @param _partService
* @param options Modal options
*/
constructor(
private _title: string,
private _name: string,
private _partService: IPartService,
private _telemetryService: ITelemetryService,
protected _clipboardService: IClipboardService,
protected _themeService: IThemeService,
_contextKeyService: IContextKeyService,
options?: IModalOptions
) {
super();
this._modalOptions = options || Object.create(null);
mixin(this._modalOptions, defaultOptions, false);
this._staticKey = generateUuid();
this._modalShowingContext = MODAL_SHOWING_CONTEXT.bindTo(_contextKeyService);
this._footerButtons = [];
}
/**
* Build and render the modal, will call {@link Modal#renderBody}
*/
public render() {
let modalBodyClass = (this._modalOptions.isAngular === false ? 'modal-body' : 'modal-body-and-footer');
let parts: Array<HTMLElement> = [];
// This modal header section refers to the header of of the dialog
// will not be rendered if the title is passed in as undefined
if (this._title !== undefined) {
this._modalHeaderSection = $().div({ class: 'modal-header' }, (modalHeader) => {
if (this._modalOptions.hasBackButton) {
modalHeader.div({ class: 'modal-go-back' }, (cellContainer) => {
this._backButton = new Button(cellContainer);
this._backButton.icon = 'backButtonIcon';
this._backButton.title = localize('modalBack', "Back");
});
}
if (this._modalOptions.hasTitleIcon) {
modalHeader.div({ class: 'modal-title-icon' }, (modalIcon) => {
this._modalTitleIcon = modalIcon.getHTMLElement();
});
}
modalHeader.div({ class: 'modal-title' }, (modalTitle) => {
this._modalTitle = modalTitle;
modalTitle.text(this._title);
});
});
parts.push(this._modalHeaderSection.getHTMLElement());
}
if (this._modalOptions.isAngular === false && this._modalOptions.hasErrors) {
this._modalMessageSection = $().div({ class: 'dialog-message error' }, (messageContainer) => {
messageContainer.div({ class: 'dialog-message-header' }, (headerContainer) => {
headerContainer.div({ class: 'dialog-message-icon' }, (iconContainer) => {
this._messageIcon = iconContainer.getHTMLElement();
});
headerContainer.div({ class: 'dialog-message-severity' }, (messageSeverityContainer) => {
this._messageSeverity = messageSeverityContainer;
});
headerContainer.div({ class: 'dialog-message-button' }, (buttonContainer) => {
this._toggleMessageDetailButton = new Button(buttonContainer);
this._toggleMessageDetailButton.icon = 'message-details-icon';
this._toggleMessageDetailButton.label = SHOW_DETAILS_TEXT;
this._toggleMessageDetailButton.onDidClick((e) => {
this.toggleMessageDetail();
});
});
headerContainer.div({ class: 'dialog-message-button' }, (buttonContainer) => {
this._copyMessageButton = new Button(buttonContainer);
this._copyMessageButton.icon = 'copy-message-icon';
this._copyMessageButton.label = COPY_TEXT;
this._copyMessageButton.onDidClick((e) => {
this._clipboardService.writeText(this.getTextForClipboard());
});
});
headerContainer.div({ class: 'dialog-message-button' }, (buttonContainer) => {
this._closeMessageButton = new Button(buttonContainer);
this._closeMessageButton.icon = 'close-message-icon';
this._closeMessageButton.label = CLOSE_TEXT;
this._closeMessageButton.onDidClick((e) => {
this.setError(undefined);
});
});
attachButtonStyler(this._toggleMessageDetailButton, this._themeService);
attachButtonStyler(this._copyMessageButton, this._themeService);
attachButtonStyler(this._closeMessageButton, this._themeService);
});
messageContainer.div({ class: 'dialog-message-body' }, (messageBody) => {
messageBody.div({ class: 'dialog-message-summary' }, (summaryContainer) => {
this._messageSummary = summaryContainer;
this._messageSummaryElement = summaryContainer.getHTMLElement();
this._messageSummaryElement.onclick = (e) => {
this.toggleMessageDetail();
};
});
messageBody.div({ class: 'dialog-message-detail' }, (detailContainer) => {
this._messageDetail = detailContainer;
this._messageDetailElement = detailContainer.getHTMLElement();
this._messageDetailElement.style.display = 'none';
});
});
});
this._messageElement = this._modalMessageSection.getHTMLElement();
this.updateElementVisibility(this._messageElement, false);
if (this._useDefaultMessageBoxLocation) {
parts.push(this._messageElement);
}
}
// This modal body section refers to the body of of the dialog
let body: Builder;
$().div({ class: modalBodyClass }, (builder) => {
body = builder;
});
this._modalBodySection = body.getHTMLElement();
parts.push(body.getHTMLElement());
this.renderBody(body.getHTMLElement());
// This modal footer section refers to the footer of of the dialog
if (this._modalOptions.isAngular === false) {
this._modalFooterSection = $().div({ class: 'modal-footer' }, (modelFooter) => {
if (this._modalOptions.hasSpinner) {
modelFooter.div({ 'class': 'icon in-progress' }, (spinnerContainer) => {
this._spinnerElement = spinnerContainer.getHTMLElement();
this._spinnerElement.style.visibility = 'hidden';
});
}
modelFooter.div({ 'class': 'left-footer' }, (leftFooter) => {
this._leftFooter = leftFooter;
});
modelFooter.div({ 'class': 'right-footer' }, (rightFooter) => {
this._rightFooter = rightFooter;
});
this._footerBuilder = modelFooter;
});
parts.push(this._modalFooterSection.getHTMLElement());
}
let builderClass = 'modal fade';
if (this._modalOptions.isFlyout) {
builderClass += ' flyout-dialog';
}
if (this._modalOptions.isWide) {
builderClass += ' wide';
}
// The builder builds the dialog. It append header, body and footer sections.
this._builder = $().div({ class: builderClass, 'role': 'dialog', 'aria-label': this._title }, (dialogContainer) => {
this._modalDialog = dialogContainer.div({ class: 'modal-dialog ', role: 'document' }, (modalDialog) => {
modalDialog.div({ class: 'modal-content' }, (modelContent) => {
parts.forEach((part) => {
modelContent.append(part);
});
});
});
});
}
/**
* Called for extended classes to render the body
* @param container The parent container to attach the rendered body to
*/
protected abstract renderBody(container: HTMLElement): void;
/**
* Overridable to change behavior of escape key
*/
protected onClose(e: StandardKeyboardEvent) {
this.hide();
}
/**
* Overridable to change behavior of enter key
*/
protected onAccept(e: StandardKeyboardEvent) {
this.hide();
}
private handleBackwardTab(e: KeyboardEvent) {
if (this._firstFocusableElement && this._lastFocusableElement && document.activeElement === this._firstFocusableElement) {
e.preventDefault();
this._lastFocusableElement.focus();
}
}
private handleForwardTab(e: KeyboardEvent) {
if (this._firstFocusableElement && this._lastFocusableElement && document.activeElement === this._lastFocusableElement) {
e.preventDefault();
this._firstFocusableElement.focus();
}
}
private getTextForClipboard(): string {
return this._messageDetailText === '' ? this._messageSummaryText : `${this._messageSummaryText}${os.EOL}========================${os.EOL}${this._messageDetailText}`;
}
private updateElementVisibility(element: HTMLElement, visible: boolean) {
element.style.display = visible ? 'block' : 'none';
}
private updateExpandMessageState() {
this._messageSummaryElement.style.cursor = this.shouldShowExpandMessageButton ? 'pointer' : 'default';
this._messageSummaryElement.classList.remove(MESSAGE_EXPANDED_MODE_CLASS);
this.updateElementVisibility(this._toggleMessageDetailButton.element, this.shouldShowExpandMessageButton);
}
private toggleMessageDetail() {
let isExpanded = this._messageSummaryElement.classList.contains(MESSAGE_EXPANDED_MODE_CLASS);
if (isExpanded) {
this._messageSummaryElement.classList.remove(MESSAGE_EXPANDED_MODE_CLASS);
this._toggleMessageDetailButton.label = SHOW_DETAILS_TEXT;
} else {
this._messageSummaryElement.classList.add(MESSAGE_EXPANDED_MODE_CLASS);
this._toggleMessageDetailButton.label = HIDE_DETAILS_TEXT;
}
if (this._messageDetailText !== '') {
this.updateElementVisibility(this._messageDetailElement, !isExpanded);
}
}
private get shouldShowExpandMessageButton(): boolean {
return this._messageDetailText !== '' || this._messageSummaryElement.scrollWidth > this._messageSummaryElement.offsetWidth;
}
/**
* Set focusable elements in the modal dialog
*/
public setFocusableElements() {
this._focusableElements = this._builder.getHTMLElement().querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
if (this._focusableElements && this._focusableElements.length > 0) {
this._firstFocusableElement = <HTMLElement>this._focusableElements[0];
this._lastFocusableElement = <HTMLElement>this._focusableElements[this._focusableElements.length - 1];
}
this._focusedElementBeforeOpen = <HTMLElement>document.activeElement;
}
/**
* Shows the modal and attaches key listeners
*/
protected show() {
this._modalShowingContext.get().push(this._staticKey);
this._builder.appendTo(document.getElementById(this._partService.getWorkbenchElementId()));
this.setFocusableElements();
this._keydownListener = DOM.addDisposableListener(document, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let context = this._modalShowingContext.get();
if (context[context.length - 1] === this._staticKey) {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter)) {
this.onAccept(event);
} else if (event.equals(KeyCode.Escape)) {
this.onClose(event);
} else if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
this.handleBackwardTab(e);
} else if (event.equals(KeyCode.Tab)) {
this.handleForwardTab(e);
}
}
});
this._resizeListener = DOM.addDisposableListener(window, DOM.EventType.RESIZE, (e: Event) => {
this.layout(DOM.getTotalHeight(this._modalBodySection));
});
this.layout(DOM.getTotalHeight(this._modalBodySection));
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ModalDialogOpened, { name: this._name });
}
/**
* Required to be implemented so that scrolling and other functions operate correctly. Should re-layout controls in the modal
*/
protected abstract layout(height: number): void;
/**
* Hides the modal and removes key listeners
*/
protected hide() {
this._modalShowingContext.get().pop();
this._builder.offDOM();
if (this._focusedElementBeforeOpen) {
this._focusedElementBeforeOpen.focus();
}
this._keydownListener.dispose();
this._resizeListener.dispose();
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ModalDialogClosed, { name: this._name });
}
/**
* Adds a button to the footer of the modal
* @param label Label to show on the button
* @param onSelect The callback to call when the button is selected
*/
protected addFooterButton(label: string, onSelect: () => void, orientation: 'left' | 'right' = 'right'): Button {
let footerButton = $('div.footer-button');
let button = new Button(footerButton);
button.label = label;
button.onDidClick(() => onSelect());
if (orientation === 'left') {
footerButton.appendTo(this._leftFooter);
} else {
footerButton.appendTo(this._rightFooter);
}
this._footerButtons.push(button);
return button;
}
/**
* Returns a footer button matching the provided label
* @param label Label to show on the button
* @param onSelect The callback to call when the button is selected
*/
protected findFooterButton(label: string): Button {
return this._footerButtons.find(e => {
try {
return e && e.element.innerText === label;
} catch {
return false;
}
});
}
/**
* Show an error in the error message element
* @param message Text to show in the message
* @param level Severity level of the message
* @param description Description of the message
*/
protected setError(message: string, level: MessageLevel = MessageLevel.Error, description: string = '') {
if (this._modalOptions.hasErrors) {
this._messageSummaryText = message ? message : '';
this._messageDetailText = description ? description : '';
if (this._messageSummaryText !== '') {
const levelClasses = ['info', 'warning', 'error'];
let selectedLevel = levelClasses[2];
let severityText = ERROR_ALT_TEXT;
if (level === MessageLevel.Information) {
selectedLevel = levelClasses[0];
severityText = INFO_ALT_TEXT;
} else if (level === MessageLevel.Warning) {
selectedLevel = levelClasses[1];
severityText = WARNING_ALT_TEXT;
}
levelClasses.forEach(level => {
if (selectedLevel === level) {
this._messageIcon.classList.add(level);
this._messageElement.classList.add(level);
} else {
this._messageIcon.classList.remove(level);
this._messageElement.classList.remove(level);
}
});
this._messageIcon.title = severityText;
this._messageSeverity.text(severityText);
this._messageSummary.text(message);
this._messageSummary.title(message);
this._messageDetail.text(description);
}
this.updateElementVisibility(this._messageDetailElement, false);
this.updateElementVisibility(this._messageElement, this._messageSummaryText !== '');
this.updateExpandMessageState();
}
}
/**
* Show the spinner element that shows something is happening, hidden by default
*/
protected showSpinner(): void {
if (this._modalOptions.hasSpinner) {
this._spinnerElement.style.visibility = 'visible';
}
}
/**
* Hide the spinner element to show that something was happening, hidden by default
*/
protected hideSpinner(): void {
if (this._modalOptions.hasSpinner) {
this._spinnerElement.style.visibility = 'hidden';
}
}
/**
* Set spinner element to show or hide
*/
public set spinner(show: boolean) {
if (show) {
this.showSpinner();
} else {
this.hideSpinner();
}
}
/**
* Return background color of header and footer
*/
protected get headerAndFooterBackground(): string {
return this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : null;
}
/**
* Set the title of the modal
* @param title
*/
protected set title(title: string) {
if (this._title !== undefined) {
this._modalTitle.text(title);
}
}
protected get title(): string {
return this._title;
}
/**
* Set the icon title class name
* @param iconClassName
*/
protected set titleIconClassName(iconClassName: string) {
if (this._modalTitleIcon) {
this._modalTitleIcon.className = 'modal-title-icon ' + iconClassName;
}
}
/**
* Called by the theme registry on theme change to style the component
*/
public style(styles: IModalDialogStyles): void {
this._dialogForeground = styles.dialogForeground;
this._dialogBorder = styles.dialogBorder;
this._dialogHeaderAndFooterBackground = styles.dialogHeaderAndFooterBackground;
this._dialogBodyBackground = styles.dialogBodyBackground;
this.applyStyles();
}
private applyStyles(): void {
const foreground = this._dialogForeground ? this._dialogForeground.toString() : null;
const border = this._dialogBorder ? this._dialogBorder.toString() : null;
const headerAndFooterBackground = this._dialogHeaderAndFooterBackground ? this._dialogHeaderAndFooterBackground.toString() : null;
const bodyBackground = this._dialogBodyBackground ? this._dialogBodyBackground.toString() : null;
ModalFooterStyle.backgroundColor = headerAndFooterBackground;
ModalFooterStyle.borderTopWidth = border ? '1px' : null;
ModalFooterStyle.borderTopStyle = border ? 'solid' : null;
ModalFooterStyle.borderTopColor = border;
if (this._closeButtonInHeader) {
this._closeButtonInHeader.style('color', foreground);
}
if (this._modalDialog) {
this._modalDialog.style('color', foreground);
this._modalDialog.style('border-width', border ? '1px' : null);
this._modalDialog.style('border-style', border ? 'solid' : null);
this._modalDialog.style('border-color', border);
}
if (this._modalHeaderSection) {
this._modalHeaderSection.style('background-color', headerAndFooterBackground);
this._modalHeaderSection.style('border-bottom-width', border ? '1px' : null);
this._modalHeaderSection.style('border-bottom-style', border ? 'solid' : null);
this._modalHeaderSection.style('border-bottom-color', border);
}
if (this._modalMessageSection) {
this._modalMessageSection.style('background-color', headerAndFooterBackground);
this._modalMessageSection.style('border-bottom-width', border ? '1px' : null);
this._modalMessageSection.style('border-bottom-style', border ? 'solid' : null);
this._modalMessageSection.style('border-bottom-color', border);
}
if (this._modalBodySection) {
this._modalBodySection.style.backgroundColor = bodyBackground;
}
if (this._modalFooterSection) {
this._modalFooterSection.style('background-color', ModalFooterStyle.backgroundColor);
this._modalFooterSection.style('border-top-width', ModalFooterStyle.borderTopWidth);
this._modalFooterSection.style('border-top-style', ModalFooterStyle.borderTopStyle);
this._modalFooterSection.style('border-top-color', ModalFooterStyle.borderTopColor);
}
}
public dispose() {
super.dispose();
this._keydownListener.dispose();
this._footerButtons = [];
}
}

View File

@@ -0,0 +1,273 @@
/*---------------------------------------------------------------------------------------------
* 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/optionsDialog';
import * as DialogHelper from './dialogHelper';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IModalOptions, Modal } from './modal';
import * as OptionsDialogHelper from './optionsDialogHelper';
import { attachButtonStyler, attachModalDialogStyler, attachPanelStyler } from 'sql/platform/theme/common/styler';
import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { ScrollableSplitView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview';
import * as sqlops from 'sqlops';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { Event, Emitter } from 'vs/base/common/event';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { localize } from 'vs/nls';
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 * as styler from 'vs/platform/theme/common/styler';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { Builder, $ } from 'vs/base/browser/builder';
import { Widget } from 'vs/base/browser/ui/widget';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IViewletPanelOptions, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class CategoryView extends ViewletPanel {
constructor(
private contentElement: HTMLElement,
private size: number,
options: IViewletPanelOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService
) {
super(options, keybindingService, contextMenuService, configurationService);
}
// we want a fixed size, so when we render to will measure our content and set that to be our
// minimum and max size
protected renderBody(container: HTMLElement): void {
container.appendChild(this.contentElement);
this.maximumBodySize = this.size;
this.minimumBodySize = this.size;
}
protected layoutBody(size: number): void {
//
}
}
export interface IOptionsDialogOptions extends IModalOptions {
cancelLabel?: string;
}
export class OptionsDialog extends Modal {
private _body: HTMLElement;
private _optionGroups: HTMLElement;
private _dividerBuilder: Builder;
private _optionTitle: Builder;
private _optionDescription: Builder;
private _optionElements: { [optionName: string]: OptionsDialogHelper.IOptionElement } = {};
private _optionValues: { [optionName: string]: string };
private _optionRowSize = 31;
private _optionCategoryPadding = 30;
private height: number;
private splitview: ScrollableSplitView;
private _onOk = new Emitter<void>();
public onOk: Event<void> = this._onOk.event;
private _onCloseEvent = new Emitter<void>();
public onCloseEvent: Event<void> = this._onCloseEvent.event;
constructor(
title: string,
name: string,
private options: IOptionsDialogOptions,
@IPartService partService: IPartService,
@IWorkbenchThemeService private _workbenchThemeService: IWorkbenchThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@IInstantiationService private _instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IClipboardService clipboardService: IClipboardService
) {
super(title, name, partService, telemetryService, clipboardService, _workbenchThemeService, contextKeyService, options);
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
if (this.backButton) {
this.backButton.onDidClick(() => this.cancel());
attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND });
}
let okButton = this.addFooterButton(localize('optionsDialog.ok', 'OK'), () => this.ok());
let closeButton = this.addFooterButton(this.options.cancelLabel || localize('optionsDialog.cancel', 'Cancel'), () => this.cancel());
// Theme styler
attachButtonStyler(okButton, this._themeService);
attachButtonStyler(closeButton, this._themeService);
this._register(this._workbenchThemeService.onDidColorThemeChange(e => this.updateTheme(e)));
this.updateTheme(this._workbenchThemeService.getColorTheme());
}
protected renderBody(container: HTMLElement) {
new Builder(container).div({ class: 'optionsDialog-options' }, (bodyBuilder) => {
this._body = bodyBuilder.getHTMLElement();
});
let builder = new Builder(this._body);
builder.div({}, (dividerContainer) => {
this._dividerBuilder = dividerContainer;
});
builder.div({ class: 'optionsDialog-description' }, (descriptionContainer) => {
descriptionContainer.div({ class: 'modal-title' }, (optionTitle) => {
this._optionTitle = optionTitle;
});
descriptionContainer.div({ class: 'optionsDialog-description-content' }, (optionDescription) => {
this._optionDescription = optionDescription;
});
});
}
// Update theming that is specific to options dialog flyout body
private updateTheme(theme: IColorTheme): void {
let borderColor = theme.getColor(contrastBorder);
let border = borderColor ? borderColor.toString() : null;
if (this._dividerBuilder) {
this._dividerBuilder.style('border-top-width', border ? '1px' : null);
this._dividerBuilder.style('border-top-style', border ? 'solid' : null);
this._dividerBuilder.style('border-top-color', border);
}
}
private onOptionLinkClicked(optionName: string): void {
let option = this._optionElements[optionName].option;
this._optionTitle.text(option.displayName);
this._optionDescription.text(option.description);
}
private fillInOptions(container: Builder, options: sqlops.ServiceOption[]): void {
for (let i = 0; i < options.length; i++) {
let option: sqlops.ServiceOption = options[i];
let rowContainer = DialogHelper.appendRow(container, option.displayName, 'optionsDialog-label', 'optionsDialog-input');
OptionsDialogHelper.createOptionElement(option, rowContainer, this._optionValues, this._optionElements, this._contextViewService, (name) => this.onOptionLinkClicked(name));
}
}
private registerStyling(): void {
// Theme styler
for (let optionName in this._optionElements) {
let widget: Widget = this._optionElements[optionName].optionWidget;
let option = this._optionElements[optionName].option;
switch (option.valueType) {
case ServiceOptionType.category:
case ServiceOptionType.boolean:
this._register(styler.attachSelectBoxStyler(<SelectBox>widget, this._themeService));
break;
case ServiceOptionType.string:
case ServiceOptionType.password:
case ServiceOptionType.number:
this._register(styler.attachInputBoxStyler(<InputBox>widget, this._themeService));
}
}
}
public get optionValues(): { [name: string]: any } {
return this._optionValues;
}
public hideError() {
this.setError('');
}
public showError(err: string) {
this.setError(err);
}
/* Overwrite escape key behavior */
protected onClose() {
this.close();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.ok();
}
public ok(): void {
if (OptionsDialogHelper.validateInputs(this._optionElements)) {
OptionsDialogHelper.updateOptions(this._optionValues, this._optionElements);
this._onOk.fire();
this.close();
}
}
public cancel() {
this.close();
}
public close() {
this._optionGroups.remove();
this.dispose();
this.hide();
this._onCloseEvent.fire();
}
public open(options: sqlops.ServiceOption[], optionValues: { [name: string]: any }) {
this._optionValues = optionValues;
let firstOption: string;
let containerGroup: Builder;
let optionsContentBuilder: Builder = $().div({ class: 'optionsDialog-options-groups monaco-panel-view' }, (container) => {
containerGroup = container;
this._optionGroups = container.getHTMLElement();
});
this.splitview = new ScrollableSplitView(containerGroup.getHTMLElement(), { enableResizing: false, scrollDebounce: 0 });
let categoryMap = OptionsDialogHelper.groupOptionsByCategory(options);
for (let category in categoryMap) {
let serviceOptions: sqlops.ServiceOption[] = categoryMap[category];
let bodyContainer = $().element('table', { class: 'optionsDialog-table' }, (tableContainer: Builder) => {
this.fillInOptions(tableContainer, serviceOptions);
});
let viewSize = this._optionCategoryPadding + serviceOptions.length * this._optionRowSize;
let categoryView = this._instantiationService.createInstance(CategoryView, bodyContainer.getHTMLElement(), viewSize, { title: category, ariaHeaderLabel: category, id: category });
this.splitview.addView(categoryView, viewSize);
categoryView.render();
attachPanelStyler(categoryView, this._themeService);
if (!firstOption) {
firstOption = serviceOptions[0].name;
}
}
if (this.height) {
this.splitview.layout(this.height - 120);
}
let body = new Builder(this._body);
body.append(optionsContentBuilder.getHTMLElement(), 0);
this.show();
let firstOptionWidget = this._optionElements[firstOption].optionWidget;
this.registerStyling();
firstOptionWidget.focus();
}
protected layout(height?: number): void {
this.height = height;
// account for padding and the details view
this.splitview.layout(this.height - 120 - 20);
}
public dispose(): void {
super.dispose();
for (let optionName in this._optionElements) {
let widget: Widget = this._optionElements[optionName].optionWidget;
widget.dispose();
delete this._optionElements[optionName];
}
}
}

View File

@@ -0,0 +1,179 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as DialogHelper from './dialogHelper';
import { Builder } from 'vs/base/browser/builder';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import * as types from 'vs/base/common/types';
import * as sqlops from 'sqlops';
import { localize } from 'vs/nls';
import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
export interface IOptionElement {
optionWidget: any;
option: sqlops.ServiceOption;
optionValue: any;
}
export function createOptionElement(option: sqlops.ServiceOption, rowContainer: Builder, options: { [name: string]: any },
optionsMap: { [optionName: string]: IOptionElement }, contextViewService: IContextViewService, onFocus: (name) => void): void {
let possibleInputs: string[] = [];
let optionValue = getOptionValueAndCategoryValues(option, options, possibleInputs);
let optionWidget: any;
let inputElement: HTMLElement;
let missingErrorMessage = localize('optionsDialog.missingRequireField', ' is required.');
let invalidInputMessage = localize('optionsDialog.invalidInput', 'Invalid input. Numeric value expected.');
if (option.valueType === ServiceOptionType.number) {
optionWidget = new InputBox(rowContainer.getHTMLElement(), contextViewService, {
validationOptions: {
validation: (value: string) => {
if (!value && option.isRequired) {
return { type: MessageType.ERROR, content: option.displayName + missingErrorMessage };
} else if (!types.isNumber(Number(value))) {
return { type: MessageType.ERROR, content: invalidInputMessage };
} else {
return null;
}
}
},
ariaLabel: option.displayName
});
optionWidget.value = optionValue;
inputElement = findElement(rowContainer, 'input');
} else if (option.valueType === ServiceOptionType.category || option.valueType === ServiceOptionType.boolean) {
optionWidget = new SelectBox(possibleInputs, optionValue.toString(), contextViewService, undefined, { ariaLabel: option.displayName });
DialogHelper.appendInputSelectBox(rowContainer, optionWidget);
inputElement = findElement(rowContainer, 'monaco-select-box');
} else if (option.valueType === ServiceOptionType.string || option.valueType === ServiceOptionType.password) {
optionWidget = new InputBox(rowContainer.getHTMLElement(), contextViewService, {
validationOptions: {
validation: (value: string) => (!value && option.isRequired) ? ({ type: MessageType.ERROR, content: option.displayName + missingErrorMessage }) : null
},
ariaLabel: option.displayName
});
optionWidget.value = optionValue;
if (option.valueType === ServiceOptionType.password) {
optionWidget.inputElement.type = 'password';
}
inputElement = findElement(rowContainer, 'input');
}
optionsMap[option.name] = { optionWidget: optionWidget, option: option, optionValue: optionValue };
inputElement.onfocus = () => onFocus(option.name);
}
export function getOptionValueAndCategoryValues(option: sqlops.ServiceOption, options: { [optionName: string]: any }, possibleInputs: string[]): any {
var optionValue = option.defaultValue;
if (options[option.name]) {
// if the value type is boolean, the option value can be either boolean or string
if (option.valueType === ServiceOptionType.boolean) {
if (options[option.name] === true || options[option.name] === trueInputValue) {
optionValue = trueInputValue;
} else {
optionValue = falseInputValue;
}
} else {
optionValue = options[option.name];
}
}
if (option.valueType === ServiceOptionType.boolean || option.valueType === ServiceOptionType.category) {
// If the option is not required, the empty string should be add at the top of possible choices
if (!option.isRequired) {
possibleInputs.push('');
}
if (option.valueType === ServiceOptionType.boolean) {
possibleInputs.push(trueInputValue, falseInputValue);
} else {
option.categoryValues.map(c => possibleInputs.push(c.name));
}
// If the option value is not set and default value is null, the option value should be set to the first possible input.
if (optionValue === null || optionValue === undefined) {
optionValue = possibleInputs[0];
}
}
return optionValue;
}
export function validateInputs(optionsMap: { [optionName: string]: IOptionElement }): boolean {
let isValid = true;
let isFocused = false;
for (var optionName in optionsMap) {
var optionElement: IOptionElement = optionsMap[optionName];
var widget = optionElement.optionWidget;
var isInputBox = (optionElement.option.valueType === ServiceOptionType.string ||
optionElement.option.valueType === ServiceOptionType.password ||
optionElement.option.valueType === ServiceOptionType.number);
if (isInputBox) {
if (!widget.validate()) {
isValid = false;
if (!isFocused) {
isFocused = true;
widget.focus();
}
}
}
}
return isValid;
}
export function updateOptions(options: { [optionName: string]: any }, optionsMap: { [optionName: string]: IOptionElement }): void {
for (var optionName in optionsMap) {
var optionElement: IOptionElement = optionsMap[optionName];
if (optionElement.optionWidget.value !== options[optionName]) {
if (!optionElement.optionWidget.value && options[optionName]) {
delete options[optionName];
}
if (optionElement.optionWidget.value) {
if (optionElement.option.valueType === ServiceOptionType.boolean) {
options[optionName] = (optionElement.optionWidget.value === trueInputValue) ? true : false;
} else {
options[optionName] = optionElement.optionWidget.value;
}
}
optionElement.optionValue = options[optionName];
}
}
}
export let trueInputValue: string = 'True';
export let falseInputValue: string = 'False';
export function findElement(container: Builder, className: string): HTMLElement {
var elementBuilder: Builder = container;
while (elementBuilder.getHTMLElement()) {
var htmlElement = elementBuilder.getHTMLElement();
if (htmlElement.className.startsWith(className)) {
break;
}
elementBuilder = elementBuilder.child(0);
}
return elementBuilder.getHTMLElement();
}
export function groupOptionsByCategory(options: sqlops.ServiceOption[]): { [category: string]: sqlops.ServiceOption[] } {
var connectionOptionsMap: { [category: string]: sqlops.ServiceOption[] } = {};
options.forEach(option => {
var groupName = option.groupName;
if (groupName === null || groupName === undefined) {
groupName = 'General';
}
if (!!connectionOptionsMap[groupName]) {
connectionOptionsMap[groupName].push(option);
} else {
connectionOptionsMap[groupName] = [option];
}
});
return connectionOptionsMap;
}

View File

@@ -5,19 +5,19 @@
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
import { IRestoreDialogController } from 'sql/platform/restore/common/restoreService';
import { IBackupUiService } from 'sql/platform/backup/common/backupService';
import { IAngularEventingService, AngularEventType } from 'sql/platform/angularEventing/common/angularEventingService';
import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
import { IInsightsDialogService } from 'sql/workbench/services/insights/common/insightsDialogService';
import { IAdminService } from 'sql/workbench/services/admin/common/adminService';
import * as Constants from 'sql/common/constants';
import { Task } from 'sql/platform/tasks/common/tasks';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
import { ObjectMetadata } from 'sqlops';

View File

@@ -12,20 +12,20 @@ import {
IConnectionCompletionOptions, ConnectionType,
RunQueryOnConnectionMode, IConnectionResult
} from 'sql/platform/connection/common/connectionManagement';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
import { EditDataInput } from 'sql/parts/editData/common/editDataInput';
import { IAdminService } from 'sql/workbench/services/admin/common/adminService';
import { IRestoreDialogController } from 'sql/platform/restore/common/restoreService';
import { IBackupUiService } from 'sql/platform/backup/common/backupService';
import { IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces';
import { IInsightsDialogService } from 'sql/workbench/services/insights/common/insightsDialogService';
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { DashboardInput } from 'sql/parts/dashboard/dashboardInput';
import { ProfilerInput } from 'sql/parts/profiler/editor/profilerInput';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
import * as sqlops from 'sqlops';

View File

@@ -23,7 +23,7 @@ import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ClearSearchAction, AddServerAction, AddServerGroupAction, ActiveConnectionsFilterAction } from 'sql/parts/objectExplorer/viewlet/connectionTreeAction';
import { warn } from 'sql/base/common/log';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';

View File

@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* 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 { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { attachButtonStyler, attachModalDialogStyler } from 'sql/platform/theme/common/styler';
import { Builder } from 'vs/base/browser/builder';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { Event, Emitter } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { localize } from 'vs/nls';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { WebviewElement } from 'vs/workbench/parts/webview/electron-browser/webviewElement';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class WebViewDialog extends Modal {
private _body: HTMLElement;
private _okButton: Button;
private _okLabel: string;
private _closeLabel: string;
private _webview: WebviewElement;
private _html: string;
private _headerTitle: string;
private _onOk = new Emitter<void>();
public onOk: Event<void> = this._onOk.event;
private _onClosed = new Emitter<void>();
public onClosed: Event<void> = this._onClosed.event;
private contentDisposables: IDisposable[] = [];
private _onMessage = new Emitter<any>();
protected contextKey: IContextKey<boolean>;
protected findInputFocusContextKey: IContextKey<boolean>;
constructor(
@IThemeService themeService: IThemeService,
@IClipboardService clipboardService: IClipboardService,
@IPartService private _webViewPartService: IPartService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService private _instantiationService: IInstantiationService
) {
super('', TelemetryKeys.WebView, _webViewPartService, telemetryService, clipboardService, themeService, contextKeyService, { isFlyout: false, hasTitleIcon: true });
this._okLabel = localize('webViewDialog.ok', 'OK');
this._closeLabel = localize('webViewDialog.close', 'Close');
}
public set html(value: string) {
this._html = value;
}
public get html(): string {
return this._html;
}
public set okTitle(value: string) {
this._okLabel = value;
}
public get okTitle(): string {
return this._okLabel;
}
public set closeTitle(value: string) {
this._closeLabel = value;
}
public get closeTitle(): string {
return this._closeLabel;
}
public set headerTitle(value: string) {
this._headerTitle = value;
}
public get headerTitle(): string {
return this._headerTitle;
}
protected renderBody(container: HTMLElement) {
new Builder(container).div({ 'class': 'webview-dialog' }, (bodyBuilder) => {
this._body = bodyBuilder.getHTMLElement();
this._webview = this._instantiationService.createInstance(WebviewElement,
this._webViewPartService.getContainer(Parts.EDITOR_PART),
this.contextKey,
this.findInputFocusContextKey,
{
enableWrappedPostMessage: true,
allowScripts: true
});
this._webview.mountTo(this._body);
this._webview.style(this._themeService.getTheme());
this._webview.onMessage(message => {
this._onMessage.fire(message);
}, null, this.contentDisposables);
this._themeService.onThemeChange(theme => this._webview.style(theme), null, this.contentDisposables);
this.contentDisposables.push(this._webview);
this.contentDisposables.push(toDisposable(() => this._webview = null));
});
}
get onMessage(): Event<any> {
return this._onMessage.event;
}
public render() {
super.render();
this._register(attachModalDialogStyler(this, this._themeService));
this._okButton = this.addFooterButton(this._okLabel, () => this.ok());
this._register(attachButtonStyler(this._okButton, this._themeService));
}
protected layout(height?: number): void {
// Nothing to re-layout
}
private updateDialogBody(): void {
this._webview.contents = this.html;
}
/* espace key */
protected onClose() {
this.ok();
}
/* enter key */
protected onAccept() {
this.ok();
}
public ok(): void {
this._onOk.fire();
this.close();
}
public close() {
this.hide();
this._onClosed.fire();
}
public sendMessage(message: any): void {
this._webview.sendMessage(message);
}
public open() {
this.title = this.headerTitle;
this.updateDialogBody();
this.show();
this._okButton.focus();
}
public dispose(): void {
this.contentDisposables.forEach(element => {
element.dispose();
});
}
}

View File

@@ -17,8 +17,9 @@ 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/base/browser/ui/modal/optionsDialog';
import { IBackupUiService, IBackupService, TaskExecutionMode } from 'sql/platform/backup/common/backupService';
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;

View 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();
}

View File

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

View File

@@ -0,0 +1,196 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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;
}
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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);
}
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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();
}
}

View File

@@ -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);
}
}

View File

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

View File

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

View File

@@ -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[];
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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);
}
}

View 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;
}
}

View File

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

View File

@@ -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();
}
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

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

View File

@@ -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 = /\${(.*?)\}/;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'
});
}
}