Compare commits

..

18 Commits

Author SHA1 Message Date
Karl Burtram
8c2d79e9cf Update SQL Tools to 1.4.0-alpha.25 (#1304) 2018-04-30 22:48:17 -07:00
Matt Irvine
cd140b5527 Encode HTML when entered in edit data cells (#1302)
* Encode HTML when entered in edit data cells

* Use VS Code's string encoding function
2018-04-30 17:58:39 -07:00
Karl Burtram
a0456bf4f7 Remove all ID fields from telemetry (#1299)
* Remove all ID fields from telemetry

* Fix how some of the fields are blanked out

* Add back blank userId

* Disable VS Code tests broken by this change
2018-04-30 16:52:54 -07:00
Karl Burtram
55e3947cf7 Pick up Electron 1.7.12 (#1291)
* Pick up Electron 1.7.12

* Might as well go to 1.7.13

* Go back to 1.7.12 to match VS Code
2018-04-30 09:18:02 -07:00
Karl Burtram
db5156e4cd Fix app name in Report Issue dialog (#1292) 2018-04-29 13:19:09 -07:00
Leila Lali
eece0677a7 added libunwind8 to Debian dependencies (#1290)
* added libunwind8 to debian depedencies
2018-04-27 17:08:26 -07:00
Matt Irvine
24e8c20511 Simplify button logic and enable button updates for custom dialogs (#1283) 2018-04-27 16:29:18 -07:00
Leila Lali
886717d330 added dropdown and form layout to model view (#1269)
* added dropdown and form layout to model view
2018-04-27 15:43:23 -07:00
Kevin Cunnane
26b27a616a Refreshing package-lock for samples since it's stale (#1287) 2018-04-27 10:19:11 -07:00
Karl Burtram
0c663e5555 Add AppInsights context flag to envelope (#1282)
* Add AppInsights context flag to envelope

* Fix typo

* Use add processor instead of patching
2018-04-26 21:26:51 -07:00
Aditya Bist
0f087915f6 Agent/loading icon (#1263)
* added progress wheel to show jobs loading

* added loading wheel to jobs view page too
2018-04-26 15:01:23 -07:00
Anthony Dresser
a78fa9c0f2 Scroll properties (#1244)
* properties isn't scrolling

* working on edge cases

* formatting

* formatting

* formatting
2018-04-26 14:10:08 -07:00
Karl Burtram
b1752ea635 Show all available extensions in Extension Manager (#1273)
* Show all available extensions in Extension Manager

* Change name of functions

* Minor cleanup
2018-04-26 10:08:02 -07:00
Matt Irvine
ec150917c2 Fix button handle bug and add tests (#1267) 2018-04-25 16:22:54 -07:00
Karl Burtram
7a9a69c439 Add TRUNCATE to the keyword colorization list (#1270)
* Add TRUNCATE to the keyword colorization list

* Add DISTINCT to keyword list
2018-04-25 16:07:46 -07:00
Aditya Bist
9e9862c6f0 Agent action icons fix (#1255)
* change icon opacity based on job status

* rid of magic numbers
2018-04-25 12:20:46 -07:00
Karl Burtram
7b76d929cd Update README and CHANGELOG for April release (#1257) 2018-04-24 22:39:01 -07:00
Matt Irvine
1811dfa423 Expose custom dialog extension APIs (#1206) 2018-04-24 16:43:14 -07:00
58 changed files with 4323 additions and 1921 deletions

View File

@@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron"
target "1.7.11"
target "1.7.12"
runtime "electron"

View File

@@ -1,5 +1,20 @@
# Change Log
## Version 0.28.6
* Release date: April 25, 2018
* Release status: Public Preview
## What's new in this version
The April Public Preview release contains some of the following highlights.
* Improvements to SQL Agent *Preview* extension
* Accessibility improvements for keyboard navigation, screen reader support and high-contrast mode.
* Improved large and protected file support for saving Admin protected and >256M files within SQL Ops Studio
* Integrated Terminal splitting to work with multiple open terminals at once
* Reduced installation on-disk file count foot print for faster installs and startup times
* Improvements to Server Reports extension
* Continue to fix GitHub issues
## Version 0.27.3
* Release date: March 28, 2017
* Release status: Public Preview

View File

@@ -8,12 +8,12 @@ SQL Operations Studio is a data management tool that enables you to work with SQ
Platform | Link
-- | --
Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=870837
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=870838
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=870839
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=870840
Linux DEB | https://go.microsoft.com/fwlink/?linkid=870842
Linux RPM | https://go.microsoft.com/fwlink/?linkid=870841
Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=872717
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=872718
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=872719
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=872720
Linux DEB | https://go.microsoft.com/fwlink/?linkid=872722
Linux RPM | https://go.microsoft.com/fwlink/?linkid=872721
Go to our [download page](https://aka.ms/sqlopsstudio) for more specific instructions.
@@ -21,14 +21,6 @@ Try out the latest insiders build from `master` at https://github.com/Microsoft/
See the [change log](https://github.com/Microsoft/sqlopsstudio/blob/master/CHANGELOG.md) for additional details of what's in this release.
**Design Discussions**
The SQL Operations Studio team would like to incorporate community feedback earlier in the development process. To facilitate this, we'd like to share our designs while features are actively being built.
We're currently collecting input on the **SQL Agent** experience and enhancements to the Manage Dashboard that we're calling **"Command Center"**. We'll add additional design feedback requests below as we start work in new feature areas. Please leave comments on these issues to help us understand your requirements and shape feature development.
* [#750 Seeking community feedback on SQL Agent UX prototype](https://github.com/Microsoft/sqlopsstudio/issues/750)
**Feature Highlights**
- Cross-Platform DB management for Windows, macOS and Linux with simple XCopy deployment

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "1.4.0-alpha.23",
"version": "1.4.0-alpha.25",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp2.1.zip",
"Windows_64": "win-x64-netcoreapp2.1.zip",

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "sqlops",
"version": "0.28.6",
"version": "0.29.1",
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
"author": {
"name": "Microsoft Corporation"

View File

@@ -1,7 +1,7 @@
Package: @@NAME@@
Version: @@VERSION@@
Section: devel
Depends: libnotify4, libnss3, gnupg, apt, libxkbfile1, libgconf-2-4, libsecret-1-0
Depends: libnotify4, libnss3, gnupg, apt, libxkbfile1, libgconf-2-4, libsecret-1-0, libunwind8
Priority: optional
Architecture: @@ARCHITECTURE@@
Maintainer: Microsoft Corporation

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Directive, Inject, forwardRef, ElementRef } from '@angular/core';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { getContentHeight, addDisposableListener, EventType } from 'vs/base/browser/dom';
import { AngularDisposable } from 'sql/base/common/lifecycle';
@Directive({
selector: '[scrollable]'
})
export class ScrollableDirective extends AngularDisposable {
private scrollableElement: ScrollableElement;
private parent: HTMLElement;
private scrolled: HTMLElement;
constructor(
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
) {
super();
this.scrolled = this._el.nativeElement as HTMLElement;
this.parent = this.scrolled.parentElement;
this.parent.removeChild(this.scrolled);
this.scrolled.style.position = 'relative';
this.scrollableElement = new ScrollableElement(this.scrolled, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false
});
this.scrollableElement.onScroll(e => {
this.scrolled.style.bottom = e.scrollTop + 'px';
});
this.parent.appendChild(this.scrollableElement.getDomNode());
const initialHeight = getContentHeight(this.scrolled);
this.scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this.scrolled),
height: getContentHeight(this.parent)
});
this._register(addDisposableListener(window, EventType.RESIZE, () => {
this.resetScrollDimensions();
}));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => {
let currentheight = getContentHeight(this.scrolled);
if (initialHeight !== currentheight) {
this.scrollableElement.setScrollDimensions({
scrollHeight: currentheight,
height: getContentHeight(this.parent)
});
}
}, 200);
}
private resetScrollDimensions() {
this.scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this.scrolled),
height: getContentHeight(this.parent)
});
}
public layout() {
}
}

View File

@@ -88,19 +88,6 @@ export function parseNumAsTimeString(value: number): string {
return tempVal > 0 ? rs + '.' + mss : rs;
}
/**
* Converts <, >, &, ", ', and any characters that are outside \u00A0 to numeric HTML entity values
* like &#123;
* (Adapted from http://stackoverflow.com/a/18750001)
* @param str String to convert
* @return String with characters replaced.
*/
export function htmlEntities(str: string): string {
return typeof (str) === 'string'
? str.replace(/[\u00A0-\u9999<>\&"']/gim, (i) => { return `&#${i.charCodeAt(0)};`; })
: undefined;
}
export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string {
let prefix = purpose ? uriPrefixes[purpose] : uriPrefixes.default;
let uri = generateUriWithPrefix(connection, prefix);

View File

@@ -5,33 +5,37 @@
import 'vs/css!./dashboardHomeContainer';
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild } from '@angular/core';
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild, ContentChild } from '@angular/core';
import { DashboardWidgetContainer } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.component';
import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularEventType } from '../../../services/angularEventing/angularEventingService';
import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/contents/dashboardWidgetWrapper.component';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
@Component({
selector: 'dashboard-home-container',
providers: [{ provide: DashboardTab, useExisting: forwardRef(() => DashboardHomeContainer) }],
template: `
<div class="fullsize" style="display: flex; flex-direction: column">
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
</dashboard-widget-wrapper>
<widget-content style="flex: 1" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
<div scrollable>
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
</dashboard-widget-wrapper>
<widget-content style="flex: 1" [scrollContent]="false" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
</div>
</div>
`
})
export class DashboardHomeContainer extends DashboardWidgetContainer {
@Input() private properties: WidgetConfig;
@ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper;
@ContentChild(ScrollableDirective) private _scrollable;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@@ -56,4 +60,9 @@ export class DashboardHomeContainer extends DashboardWidgetContainer {
}
});
}
public layout() {
super.layout();
this._scrollable.layout();
}
}

View File

@@ -75,6 +75,7 @@ export class WidgetContent extends AngularDisposable implements AfterViewInit {
@Input() private widgets: WidgetConfig[];
@Input() private originalConfig: WidgetConfig[];
@Input() private context: string;
@Input() private scrollContent = true;
private _scrollableElement: ScrollableElement;
@@ -123,41 +124,43 @@ export class WidgetContent extends AngularDisposable implements AfterViewInit {
}
ngAfterViewInit() {
let container = this._scrollContainer.nativeElement as HTMLElement;
let scrollable = this._scrollable.nativeElement as HTMLElement;
container.removeChild(scrollable);
if (this.scrollContent) {
let container = this._scrollContainer.nativeElement as HTMLElement;
let scrollable = this._scrollable.nativeElement as HTMLElement;
container.removeChild(scrollable);
this._scrollableElement = new ScrollableElement(scrollable, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false
});
this._scrollableElement = new ScrollableElement(scrollable, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false
});
this._scrollableElement.onScroll(e => {
scrollable.style.bottom = e.scrollTop + 'px';
});
this._scrollableElement.onScroll(e => {
scrollable.style.bottom = e.scrollTop + 'px';
});
container.appendChild(this._scrollableElement.getDomNode());
let initalHeight = getContentHeight(scrollable);
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
container.appendChild(this._scrollableElement.getDomNode());
let initalHeight = getContentHeight(scrollable);
this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container)
});
this._register(addDisposableListener(window, EventType.RESIZE, () => {
this.resetScrollDimensions();
}));
this._register(addDisposableListener(window, EventType.RESIZE, () => {
this.resetScrollDimensions();
}));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => {
let currentheight = getContentHeight(scrollable);
if (initalHeight !== currentheight) {
this._scrollableElement.setScrollDimensions({
scrollHeight: currentheight,
height: getContentHeight(container)
});
}
}, 200);
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => {
let currentheight = getContentHeight(scrollable);
if (initalHeight !== currentheight) {
this._scrollableElement.setScrollDimensions({
scrollHeight: currentheight,
height: getContentHeight(container)
});
}
}, 200);
}
}
public layout() {
@@ -167,7 +170,9 @@ export class WidgetContent extends AngularDisposable implements AfterViewInit {
});
}
this._grid.triggerResize();
this.resetScrollDimensions();
if (this.scrollContent) {
this.resetScrollDimensions();
}
}
private resetScrollDimensions() {

View File

@@ -54,9 +54,10 @@ import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.comp
import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component';
let baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer,
DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent,
ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer,
JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, DashboardModelViewContainer, ModelComponentWrapper];
DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent,
ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer,
JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, DashboardModelViewContainer, ModelComponentWrapper,
ScrollableDirective];
/* Panel */
import { PanelModule } from 'sql/base/browser/ui/panel/panel.module';
@@ -74,6 +75,7 @@ import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.compo
import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component';
import { WebviewWidget } from 'sql/parts/dashboard/widgets/webview/webviewWidget.component';
import { JobStepsViewComponent } from '../jobManagement/views/jobStepsView.component';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
let widgetComponents = [
PropertiesWidgetComponent,

View File

@@ -3,14 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Utils from 'sql/parts/connection/common/utils';
import * as Strings from 'vs/base/common/strings';
export class DBCellValue {
displayValue: string;
isNull: boolean;
public static isDBCellValue(object: any): boolean {
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
}
}
@@ -25,7 +25,7 @@ export function hyperLinkFormatter(row: number, cell: any, value: any, columnDef
valueToDisplay = 'NULL';
if (!value.isNull) {
cellClasses += ' xmlLink';
valueToDisplay = Utils.htmlEntities(value.displayValue);
valueToDisplay = Strings.escape(value.displayValue);
return `<a class="${cellClasses}" href="#" >${valueToDisplay}</a>`;
} else {
cellClasses += ' missing-value';
@@ -44,13 +44,12 @@ export function textFormatter(row: number, cell: any, value: any, columnDef: any
if (DBCellValue.isDBCellValue(value)) {
valueToDisplay = 'NULL';
if (!value.isNull) {
valueToDisplay = Utils.htmlEntities(value.displayValue.replace(/(\r\n|\n|\r)/g, ' '));
valueToDisplay = Strings.escape(value.displayValue.replace(/(\r\n|\n|\r)/g, ' '));
} else {
cellClasses += ' missing-value';
}
} else if (typeof value === 'string'){
valueToDisplay = value;
} else if (typeof value === 'string') {
valueToDisplay = Strings.escape(value);
}
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;

View File

@@ -9,6 +9,9 @@ import * as nls from 'vs/nls';
export class AgentJobUtilities {
public static startIconClass: string = 'icon-start';
public static stopIconClass: string = 'icon-stop';
public static convertToStatusString(status: number): string {
switch(status) {
case(0): return nls.localize('agentUtilities.failed','Failed');
@@ -51,4 +54,41 @@ export class AgentJobUtilities {
return date;
}
}
public static setRunnable(icon: HTMLElement, index: number) {
if (icon.className.includes('non-runnable')) {
icon.className = icon.className.slice(0, index);
}
}
public static getActionIconClassName(startIcon: HTMLElement, stopIcon: HTMLElement, executionStatus: number) {
this.setRunnable(startIcon, AgentJobUtilities.startIconClass.length);
this.setRunnable(stopIcon, AgentJobUtilities.stopIconClass.length);
switch (executionStatus) {
case(1): // executing
startIcon.className += ' non-runnable';
return;
case(2): // Waiting for thread
startIcon.className += ' non-runnable';
return;
case(3): // Between retries
startIcon.className += ' non-runnable';
return;
case(4): //Idle
stopIcon.className += ' non-runnable';
return;
case(5): // Suspended
stopIcon.className += ' non-runnable';
return;
case(6): //obsolete
startIcon.className += ' non-runnable';
stopIcon.className += ' non-runnable';
return;
case(7): //Performing Completion Actions
startIcon.className += ' non-runnable';
return;
default:
return;
}
}
}

View File

@@ -22,8 +22,9 @@ jobhistory-component {
}
.vs-dark .job-heading-container {
height: 32px;
height: 49px;
border-bottom: 3px solid #444444;
display: -webkit-box;
}
#jobsDiv .jobview-grid {
@@ -182,4 +183,11 @@ jobsview-component .jobview-grid > .monaco-table .slick-viewport > .grid-canvas
.vs-dark .jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white;
}
.job-heading-container > .icon.in-progress {
height: 20px;
width: 20px;
padding-top: 16px;
padding-left: 15px;
}

View File

@@ -4,8 +4,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="jobhistory-heading-container">
<h1 class="job-heading">Jobs | {{this._agentJobInfo?.name}} </h1>
<div class="icon in-progress" *ngIf="showProgressWheel()"></div>
</div>
<!-- Back -->
<div class="all-jobs">

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./jobHistory';
import 'vs/css!sql/media/icons/common-icons';
import { OnInit, OnChanges, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core';
import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -60,6 +61,7 @@ export class JobHistoryComponent extends Disposable implements OnInit {
private _jobCacheObject: JobCacheObject;
private _notificationService: INotificationService;
private _agentJobInfo: AgentJobInfo;
private _noJobsAvailable: boolean = false;
constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService,
@@ -131,6 +133,7 @@ export class JobHistoryComponent extends Disposable implements OnInit {
this._agentJobInfo = this._agentViewComponent.agentJobInfo;
if (!this.agentJobInfo) {
this.agentJobInfo = this._agentJobInfo;
this.setActions();
}
if (this._isVisible === false && this._tableContainer.nativeElement.offsetParent !== null) {
this._isVisible = true;
@@ -145,7 +148,10 @@ export class JobHistoryComponent extends Disposable implements OnInit {
} else if (jobHistories && jobHistories.length === 0 ){
this._showPreviousRuns = false;
this._showSteps = false;
this._noJobsAvailable = true;
this._cd.detectChanges();
} else {
this.loadHistory();
}
this._jobCacheObject.prevJobID = this._agentViewComponent.jobId;
} else if (this._isVisible === true && this._agentViewComponent.refresh) {
@@ -273,6 +279,16 @@ export class JobHistoryComponent extends Disposable implements OnInit {
return time.replace('T', ' ');
}
private showProgressWheel(): boolean {
return this._showPreviousRuns !== true && this._noJobsAvailable === false;
}
private setActions(): void {
let startIcon: HTMLElement = $('.icon-start').get(0);
let stopIcon: HTMLElement = $('.icon-stop').get(0);
AgentJobUtilities.getActionIconClassName(startIcon, stopIcon, this.agentJobInfo.currentExecutionStatus);
}
public get showSteps(): boolean {
return this._showSteps;
}

View File

@@ -29,7 +29,6 @@ ul.action-buttons li {
padding-right: 25px;
display: inline-block;
width: 50px;
cursor: pointer;
}
.overview-container .overview-tab .resultsViewCollapsible {
@@ -134,6 +133,7 @@ input#accordion:checked ~ .accordion-content {
width: 20px;
background-image: url('../common/media/start.svg');
background-repeat: no-repeat;
cursor: pointer;
}
.vs ul.action-buttons .icon-stop,
@@ -144,6 +144,17 @@ input#accordion:checked ~ .accordion-content {
background-repeat: no-repeat;
height: 20px;
width: 20px;
cursor: pointer;
}
ul.action-buttons div.icon-start.non-runnable {
opacity: 0.4;
cursor: default;
}
ul.action-buttons div.icon-stop.non-runnable {
opacity: 0.4;
cursor: default;
}
.accordion-content #col1,
@@ -240,4 +251,15 @@ table.step-list tr.step-row td {
jobhistory-component .history-details .step-table.prev-run-list .monaco-scrollable-element {
overflow-y: scroll !important;
}
jobhistory-component .jobhistory-heading-container {
display: -webkit-box;
}
jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
width: 20px;
height: 20px;
padding-top: 16px;
padding-left: 20px;
}

View File

@@ -7,6 +7,7 @@
<div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Jobs</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Jobs Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div>
<div #jobsgrid class="jobview-grid"></div>

View File

@@ -9,6 +9,7 @@ import 'vs/css!sql/parts/grid/media/styles';
import 'vs/css!sql/parts/grid/media/slick.grid';
import 'vs/css!sql/parts/grid/media/slickGrid';
import 'vs/css!../common/media/jobs';
import 'vs/css!sql/media/icons/common-icons';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core';
import * as Utils from 'sql/parts/connection/common/utils';
@@ -72,6 +73,7 @@ export class JobsViewComponent implements AfterContentChecked {
public jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = Object.create(null);
private _serverName: string;
private _isCloud: boolean;
private _showProgressWheel: boolean;
constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService,
@@ -99,18 +101,22 @@ export class JobsViewComponent implements AfterContentChecked {
this.isVisible = true;
if (!this.isInitialized) {
if (this._jobCacheObject.serverName === this._serverName && this._jobCacheObject.jobs.length > 0) {
this._showProgressWheel = true;
this.jobs = this._jobCacheObject.jobs;
this.onFirstVisible(true);
this.isInitialized = true;
} else {
this._showProgressWheel = true;
this.onFirstVisible(false);
this.isInitialized = true;
}
}
} else if (this.isVisible === true && this._agentViewComponent.refresh === true) {
this._showProgressWheel = true;
this.onFirstVisible(false);
this._agentViewComponent.refresh = false;
} else if (this.isVisible === true && this._agentViewComponent.refresh === false) {
this._showProgressWheel = true;
this.onFirstVisible(true);
} else if (this.isVisible === true && this._gridEl.nativeElement.offsetParent === null) {
this.isVisible = false;
@@ -219,6 +225,8 @@ export class JobsViewComponent implements AfterContentChecked {
let currentTarget = e.currentTarget;
currentTarget.title = currentTarget.innerText;
});
this._showProgressWheel = false;
this._cd.detectChanges();
this.loadJobHistories();
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
import * as sqlops from 'sqlops';
import Event, { Emitter } from 'vs/base/common/event';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { attachButtonStyler } from 'sql/common/theme/styler';
import { Button } from 'sql/base/browser/ui/button/button';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
@Component({
selector: 'button',
template: `
<div #input style="width: 100%"></div>
`
})
export default class ButtonComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _button: Button;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this._button = new Button(this._inputContainer.nativeElement);
this._register(this._button);
this._register(attachButtonStyler(this._button, this._commonService.themeService, {
buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND
}));
this._register(this._button.onDidClick(e => {
this._onEventEmitter.fire({
eventType: ComponentEventType.onDidClick,
args: e
});
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
this._changeRef.detectChanges();
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._button.label = this.label;
}
// CSS-bound properties
private get label(): string {
return this.getPropertyOrDefault<sqlops.ButtonProperties, string>((props) => props.label, '');
}
private set label(newValue: string) {
this.setPropertyFromUI<sqlops.ButtonProperties, string>(this.setValueProperties, newValue);
}
private setValueProperties(properties: sqlops.ButtonProperties, label: string): void {
properties.label = label;
}
}

View File

@@ -75,7 +75,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
return types.isUndefinedOrNull(property) ? defaultVal : property;
}
protected setProperty<TPropertyBag, TValue>(propertySetter: (TPropertyBag, TValue) => void, value: TValue) {
protected setPropertyFromUI<TPropertyBag, TValue>(propertySetter: (TPropertyBag, TValue) => void, value: TValue) {
propertySetter(this.getProperties<TPropertyBag>(), value);
this._onEventEmitter.fire({
eventType: ComponentEventType.PropertiesChanged,
@@ -86,6 +86,12 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
public get onEvent(): Event<IComponentEventArgs> {
return this._onEventEmitter.event;
}
public get title(): string {
let properties = this.getProperties();
let title = properties['title'];
return title ? <string>title : '';
}
}
export abstract class ContainerBase<T> extends ComponentBase {

View File

@@ -4,16 +4,28 @@
*--------------------------------------------------------------------------------------------*/
import FlexContainer from './flexContainer.component';
import FormContainer from './formContainer.component';
import CardComponent from './card.component';
import InputBoxComponent from './inputbox.component';
import DropDownComponent from './dropdown.component';
import ButtonComponent from './button.component';
import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
export const FLEX_CONTAINER = 'flex-container';
registerComponentType(FLEX_CONTAINER, ModelComponentTypes.FlexContainer, FlexContainer);
export const FORM_CONTAINER = 'form-container';
registerComponentType(FORM_CONTAINER, ModelComponentTypes.Form, FormContainer);
export const CARD_COMPONENT = 'card-component';
registerComponentType(CARD_COMPONENT, ModelComponentTypes.Card, CardComponent);
export const INPUTBOX_COMPONENT = 'inputbox-component';
registerComponentType(INPUTBOX_COMPONENT, ModelComponentTypes.InputBox, InputBoxComponent);
export const DROPDOWN_COMPONENT = 'dropdown-component';
registerComponentType(DROPDOWN_COMPONENT, ModelComponentTypes.DropDown, DropDownComponent);
export const BUTTON_COMPONENT = 'button-component';
registerComponentType(BUTTON_COMPONENT, ModelComponentTypes.Button, ButtonComponent);

View File

@@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
import * as sqlops from 'sqlops';
import Event, { Emitter } from 'vs/base/common/event';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { Dropdown, IDropdownOptions } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { attachEditableDropdownStyler } from 'sql/common/theme/styler';
@Component({
selector: 'inputBox',
template: `
<div #input style="width: 100%"></div>
`
})
export default class DropDownComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _dropdown: Dropdown;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
let dropdownOptions: IDropdownOptions = {
values: [],
strictSelection: false,
placeholder: '',
maxHeight: 125,
ariaLabel: ''
};
this._dropdown = new Dropdown(this._inputContainer.nativeElement, this._commonService.contextViewService, this._commonService.themeService,
dropdownOptions);
this._register(this._dropdown);
this._register(attachEditableDropdownStyler(this._dropdown, this._commonService.themeService));
this._register(this._dropdown.onValueChange(e => {
this.value = this._dropdown.value;
this._onEventEmitter.fire({
eventType: ComponentEventType.onDidChange,
args: e
});
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
this._changeRef.detectChanges();
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._dropdown.values = this.values ? this.values : [];
if (this.value) {
this._dropdown.value = this.value;
}
}
// CSS-bound properties
private get value(): string {
return this.getPropertyOrDefault<sqlops.DropDownProperties, string>((props) => props.value, '');
}
private set value(newValue: string) {
this.setPropertyFromUI<sqlops.DropDownProperties, string>(this.setValueProperties, newValue);
}
private get values(): string[] {
return this.getPropertyOrDefault<sqlops.DropDownProperties, string[]>((props) => props.values, undefined);
}
private set values(newValue: string[]) {
this.setPropertyFromUI<sqlops.DropDownProperties, string[]>(this.setValuesProperties, newValue);
}
private setValueProperties(properties: sqlops.DropDownProperties, value: string): void {
properties.value = value;
}
private setValuesProperties(properties: sqlops.DropDownProperties, values: string[]): void {
properties.values = values;
}
}

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 'vs/css!./formLayout';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { FormLayout, FormItemLayout } from 'sqlops';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ContainerBase } from 'sql/parts/modelComponents/componentBase';
import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
export interface TitledFormItemLayout {
title: string;
actions?: string[];
isFormComponent: Boolean;
}
class FormItem {
constructor(public descriptor: IComponentDescriptor, public config: TitledFormItemLayout) { }
}
@Component({
template: `
<div #container *ngIf="items" class="form-table"
[style.alignItems]="alignItems" [style.alignContent]="alignContent">
<div *ngFor="let item of items" class="form-row">
<ng-container *ngIf="isFormComponent(item)">
<div class="form-cell">{{getItemTitle(item)}}</div>
<div class="form-cell">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
<div *ngIf="itemHasActions(item)" class="form-cell">
<div *ngFor="let actionItem of getActionComponents(item)" >
<model-component-wrapper [descriptor]="actionItem.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
</ng-container>
</div>
</div>
`
})
export default class FormContainer extends ContainerBase<FormItemLayout> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _alignItems: string;
private _alignContent: string;
@ViewChildren(ModelComponentWrapper) private _componentWrappers: QueryList<ModelComponentWrapper>;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
constructor (
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
ngAfterViewInit(): void {
}
/// IComponent implementation
public layout(): void {
if (this._componentWrappers) {
this._componentWrappers.forEach(wrapper => {
wrapper.layout();
});
}
}
public get alignItems(): string {
return this._alignItems;
}
public get alignContent(): string {
return this._alignContent;
}
private getItemTitle(item: FormItem): string {
let itemConfig = item.config;
return itemConfig ? itemConfig.title : '';
}
private getActionComponents(item: FormItem): FormItem[]{
let items = this.items;
let itemConfig = item.config;
if (itemConfig && itemConfig.actions) {
let resultItems = itemConfig.actions.map(x => {
let actionComponent = items.find(i => i.descriptor.id === x);
return <FormItem>actionComponent;
});
return resultItems.filter(r => r && r.descriptor);
}
return [];
}
private isFormComponent(item: FormItem): Boolean {
return item && item.config && item.config.isFormComponent;
}
private itemHasActions(item: FormItem): Boolean {
let itemConfig = item.config;
return itemConfig && itemConfig.actions !== undefined && itemConfig.actions.length > 0;
}
public setLayout(layout: any): void {
this.layout();
}
}

View File

@@ -0,0 +1,20 @@
.form-table {
width:400px;
display:table;
padding: 30px;
}
.form-row {
display: table-row;
width: 100px;
}
.form-cell {
padding: 5px;
display: table-cell;
}
.form-action {
width: 20px;
}

View File

@@ -3,7 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
@@ -70,7 +71,7 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
this._changeRef.detectChanges();
}
public setLayout (layout: any): void {
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
@@ -87,7 +88,7 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
}
public set value(newValue: string) {
this.setProperty<sqlops.InputBoxProperties, string>(this.setInputBoxProperties, newValue);
this.setPropertyFromUI<sqlops.InputBoxProperties, string>(this.setInputBoxProperties, newValue);
}
private setInputBoxProperties(properties: sqlops.InputBoxProperties, value: string): void {

View File

@@ -21,6 +21,7 @@ export interface IComponent {
addToContainer?: (componentDescriptor: IComponentDescriptor, config: any) => void;
setLayout?: (layout: any) => void;
setProperties?: (properties: { [key: string]: any; }) => void;
title?: string;
onEvent?: Event<IComponentEventArgs>;
}
@@ -53,11 +54,13 @@ export interface IComponentDescriptor {
export interface IComponentEventArgs {
eventType: ComponentEventType;
args: any;
componentId?: string;
}
export enum ComponentEventType {
PropertiesChanged,
onDidChange
onDidChange,
onDidClick
}
export interface IModelStore {

View File

@@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import nls = require('vs/nls');
import * as sqlops from 'sqlops';
import { IModelStore, IComponentDescriptor, IComponent } from './interfaces';
import { IModelStore, IComponentDescriptor, IComponent, IComponentEventArgs } from './interfaces';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IModelView } from 'sql/services/model/modelViewService';
import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
@@ -18,7 +18,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle';
import { ModelStore } from 'sql/parts/modelComponents/modelStore';
import Event, { Emitter } from 'vs/base/common/event';
const componentRegistry = <IComponentRegistry> Registry.as(Extensions.ComponentContribution);
const componentRegistry = <IComponentRegistry>Registry.as(Extensions.ComponentContribution);
/**
* Provides common logic required for any implementation that hooks to a model provided by
@@ -57,7 +57,7 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
this.setLayout(component.id, component.layout);
this.registerEvent(component.id);
if (component.itemConfigs) {
for(let item of component.itemConfigs) {
for (let item of component.itemConfigs) {
this.addToContainer(component.id, item);
}
}
@@ -66,12 +66,12 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
}
clearContainer(componentId: string): void {
this.queueAction(componentId, (component) => component.clearContainer());
this.queueAction(componentId, (component) => component.clearContainer());
}
addToContainer(containerId: string, itemConfig: IItemConfig): void {
// Do not return the promise as this should be non-blocking
this.queueAction(containerId, (component) => {
this.queueAction(containerId, (component) => {
let childDescriptor = this.defineComponent(itemConfig.componentShape);
component.addToContainer(childDescriptor, itemConfig.config);
});
@@ -81,14 +81,14 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
if (!layout) {
return;
}
this.queueAction(componentId, (component) => component.setLayout(layout));
this.queueAction(componentId, (component) => component.setLayout(layout));
}
setProperties(componentId: string, properties: { [key: string]: any; }): void {
if (!properties) {
return;
}
this.queueAction(componentId, (component) => component.setProperties(properties));
this.queueAction(componentId, (component) => component.setProperties(properties));
}
private queueAction<T>(componentId: string, action: (component: IComponent) => T): void {
@@ -98,16 +98,17 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
}
registerEvent(componentId: string) {
this.queueAction(componentId, (component) => {
this.queueAction(componentId, (component) => {
if (component.onEvent) {
this._register(component.onEvent(e => {
e.componentId = componentId;
this._onEventEmitter.fire(e);
}));
}
});
}
public get onEvent(): Event<any> {
public get onEvent(): Event<IComponentEventArgs> {
return this._onEventEmitter.event;
}
}

View File

@@ -12,14 +12,24 @@ import { Dialog } from 'sql/platform/dialog/dialogTypes';
import { IModalOptions } from 'sql/base/browser/ui/modal/modal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const defaultOptions: IModalOptions = { hasBackButton: true, isWide: true };
const defaultOptions: IModalOptions = { hasBackButton: true, isWide: false };
export class CustomDialogService {
private _dialogModals = new Map<Dialog, DialogModal>();
constructor( @IInstantiationService private _instantiationService: IInstantiationService) { }
public showDialog(dialog: Dialog, options?: IModalOptions): void {
let optionsDialog = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions);
optionsDialog.render();
optionsDialog.open();
let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions);
this._dialogModals.set(dialog, dialogModal);
dialogModal.render();
dialogModal.open();
}
public closeDialog(dialog: Dialog): void {
let dialogModal = this._dialogModals.get(dialog);
if (dialogModal) {
dialogModal.cancel();
}
}
}

View File

@@ -8,7 +8,7 @@
import 'vs/css!./media/dialogModal';
import { Modal, IModalOptions } from 'sql/base/browser/ui/modal/modal';
import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { Dialog } from 'sql/platform/dialog/dialogTypes';
import { Dialog, DialogButton } from 'sql/platform/dialog/dialogTypes';
import { DialogPane } from 'sql/platform/dialog/dialogPane';
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
import { Builder } from 'vs/base/browser/builder';
@@ -23,9 +23,6 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { localize } from 'vs/nls';
export class DialogModal extends Modal {
private static readonly DONE_BUTTON_LABEL = localize('dialogModalDoneButtonLabel', 'Done');
private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', 'Cancel');
private _dialogPane: DialogPane;
// Wizard HTML elements
@@ -61,10 +58,34 @@ export class DialogModal extends Modal {
attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND });
}
this._cancelButton = this.addFooterButton(DialogModal.CANCEL_BUTTON_LABEL, () => this.cancel());
this._doneButton = this.addFooterButton(DialogModal.DONE_BUTTON_LABEL, () => this.done());
attachButtonStyler(this._cancelButton, this._themeService);
attachButtonStyler(this._doneButton, this._themeService);
if (this._dialog.customButtons) {
this._dialog.customButtons.forEach(button => {
let buttonElement = this.addDialogButton(button);
this.updateButtonElement(buttonElement, button);
});
}
this._cancelButton = this.addDialogButton(this._dialog.cancelButton, () => this.cancel());
this.updateButtonElement(this._cancelButton, this._dialog.cancelButton);
this._doneButton = this.addDialogButton(this._dialog.okButton, () => this.done());
this.updateButtonElement(this._doneButton, this._dialog.okButton);
}
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined): Button {
let buttonElement = this.addFooterButton(button.label, onSelect);
buttonElement.enabled = button.enabled;
button.registerClickEvent(buttonElement.onDidClick);
button.onUpdate(() => {
this.updateButtonElement(buttonElement, button);
});
attachButtonStyler(buttonElement, this._themeService);
return buttonElement;
}
private updateButtonElement(buttonElement: Button, dialogButton: DialogButton) {
buttonElement.label = dialogButton.label;
buttonElement.enabled = dialogButton.enabled;
dialogButton.hidden ? buttonElement.element.classList.add('dialogModal-hidden') : buttonElement.element.classList.remove('dialogModal-hidden');
}
protected renderBody(container: HTMLElement): void {

View File

@@ -92,4 +92,9 @@ export class DialogPane extends Disposable implements IThemable {
this._body.style.backgroundColor = styles.dialogBodyBackground ? styles.dialogBodyBackground.toString() : undefined;
this._body.style.color = styles.dialogForeground ? styles.dialogForeground.toString() : undefined;
}
public dispose() {
super.dispose();
this._moduleRef.destroy();
}
}

View File

@@ -6,6 +6,7 @@
'use strict';
import * as sqlops from 'sqlops';
import { localize } from 'vs/nls';
import Event, { Emitter } from 'vs/base/common/event';
export class DialogTab implements sqlops.window.modelviewdialog.DialogTab {
@@ -16,40 +17,70 @@ export class DialogTab implements sqlops.window.modelviewdialog.DialogTab {
this.content = content;
}
}
public updateContent(): void { }
}
export class Dialog implements sqlops.window.modelviewdialog.Dialog {
public content: string | DialogTab[];
public okTitle: string;
public cancelTitle: string;
public customButtons: DialogButton[];
private static readonly DONE_BUTTON_LABEL = localize('dialogModalDoneButtonLabel', 'Done');
private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', 'Cancel');
private _onOk: Emitter<void> = new Emitter<void>();
public readonly onOk: Event<void> = this._onOk.event;
private _onCancel: Emitter<void> = new Emitter<void>();
public readonly onCancel: Event<void> = this._onCancel.event;
public content: string | DialogTab[];
public okButton: DialogButton = new DialogButton(Dialog.DONE_BUTTON_LABEL, true);
public cancelButton: DialogButton = new DialogButton(Dialog.CANCEL_BUTTON_LABEL, true);
public customButtons: DialogButton[];
constructor(public title: string, content?: string | DialogTab[]) {
if (content) {
this.content = content;
}
}
public open(): void { }
public close(): void { }
public updateContent(): void { }
}
export class DialogButton implements sqlops.window.modelviewdialog.Button {
public label: string;
public enabled: boolean;
private _label: string;
private _enabled: boolean;
private _hidden: boolean;
private _onClick: Emitter<void> = new Emitter<void>();
public readonly onClick: Event<void> = this._onClick.event;
private _onUpdate: Emitter<void> = new Emitter<void>();
public readonly onUpdate: Event<void> = this._onUpdate.event;
constructor(label: string, enabled: boolean) {
this.label = label;
this.enabled = enabled;
this._label = label;
this._enabled = enabled;
this._hidden = false;
}
public get label(): string {
return this._label;
}
public set label(label: string) {
this._label = label;
this._onUpdate.fire();
}
public get enabled(): boolean {
return this._enabled;
}
public set enabled(enabled: boolean) {
this._enabled = enabled;
this._onUpdate.fire();
}
public get hidden(): boolean {
return this._hidden;
}
public set hidden(hidden: boolean) {
this._hidden = hidden;
this._onUpdate.fire();
}
/**
* Register an event that notifies the button that it has been clicked
*/
public registerClickEvent(clickEvent: Event<void>): void {
clickEvent(() => this._onClick.fire());
}
}

View File

@@ -23,6 +23,6 @@
height: 100%;
}
.dialogModal-pane.dialogModal-hidden {
.dialogModal-hidden {
display: none;
}

View File

@@ -20,23 +20,30 @@ declare module 'sqlops' {
flexContainer(): FlexBuilder;
card(): ComponentBuilder<CardComponent>;
inputBox(): ComponentBuilder<InputBoxComponent>;
button(): ComponentBuilder<ButtonComponent>;
dropDown(): ComponentBuilder<DropDownComponent>;
dashboardWidget(widgetId: string): ComponentBuilder<WidgetComponent>;
dashboardWebview(webviewId: string): ComponentBuilder<WebviewComponent>;
formContainer(): FormBuilder;
}
export interface ComponentBuilder<T extends Component> {
component(): T;
withProperties<U>(properties: U): ComponentBuilder<T>;
}
export interface ContainerBuilder<T extends Component, TLayout,TItemLayout> extends ComponentBuilder<T> {
export interface ContainerBuilder<T extends Component, TLayout, TItemLayout> extends ComponentBuilder<T> {
withLayout(layout: TLayout): ContainerBuilder<T, TLayout, TItemLayout>;
withItems(components: Array<Component>, itemLayout ?: TItemLayout): ContainerBuilder<T, TLayout, TItemLayout>;
withItems(components: Array<Component>, itemLayout?: TItemLayout): ContainerBuilder<T, TLayout, TItemLayout>;
}
export interface FlexBuilder extends ContainerBuilder<FlexContainer, FlexLayout, FlexItemLayout> {
}
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>;
}
export interface Component {
readonly id: string;
@@ -50,10 +57,16 @@ declare module 'sqlops' {
updateProperties(properties: { [key: string]: any }): Thenable<boolean>;
}
export interface FormComponent {
component: Component;
title: string;
actions?: Component[];
}
/**
* A component that contains other components
*/
export interface Container<TLayout,TItemLayout> extends Component {
export interface Container<TLayout, TItemLayout> extends Component {
/**
* A copy of the child items array. This cannot be added to directly -
* components must be created using the create methods instead
@@ -70,7 +83,7 @@ declare module 'sqlops' {
* @param itemConfigs the definitions
* @param {*} [itemLayout] Optional layout for the child items
*/
addItems(itemConfigs: Array<Component>, itemLayout ?: TItemLayout): void;
addItems(itemConfigs: Array<Component>, itemLayout?: TItemLayout): void;
/**
* Creates a child component and adds it to this container.
@@ -78,7 +91,7 @@ declare module 'sqlops' {
* @param {Component} component the component to be added
* @param {*} [itemLayout] Optional layout for this child item
*/
addItem(component: Component, itemLayout ?: TItemLayout): void;
addItem(component: Component, itemLayout?: TItemLayout): void;
/**
* Defines the layout for this container
@@ -130,9 +143,21 @@ declare module 'sqlops' {
flex?: string;
}
export interface FormItemLayout {
}
export interface FormLayout {
}
export interface FlexContainer extends Container<FlexLayout, FlexItemLayout> {
}
export interface FormContainer extends Container<FormLayout, FormItemLayout> {
}
/**
* Describes an action to be shown in the UI, with a user-readable label
* and a callback to execute the action
@@ -153,16 +178,25 @@ declare module 'sqlops' {
* Properties representing the card component, can be used
* when using ModelBuilder to create the component
*/
export interface CardProperties {
export interface CardProperties {
label: string;
value?: string;
actions?: ActionDescriptor[];
}
export interface InputBoxProperties {
export interface InputBoxProperties {
value?: string;
}
export interface DropDownProperties {
value?: string;
values?: string[];
}
export interface ButtonProperties {
label?: string;
}
export interface CardComponent extends Component {
label: string;
value: string;
@@ -174,6 +208,17 @@ declare module 'sqlops' {
onTextChanged: vscode.Event<any>;
}
export interface DropDownComponent extends Component {
value: string;
values: string[];
onValueChanged: vscode.Event<any>;
}
export interface ButtonComponent extends Component {
label: string;
onDidClick: vscode.Event<any>;
}
export interface WidgetComponent extends Component {
widgetId: string;
}
@@ -242,6 +287,16 @@ declare module 'sqlops' {
*/
export function createButton(label: string): Button;
/**
* Opens the given dialog if it is not already open
*/
export function openDialog(dialog: Dialog): void;
/**
* Closes the given dialog if it is open
*/
export function closeDialog(dialog: Dialog): void;
// Model view dialog classes
export interface Dialog {
/**
@@ -252,79 +307,52 @@ declare module 'sqlops' {
/**
* The content of the dialog. If multiple tabs are given they will be displayed with tabs
* If a string is given, it should be the ID of the dialog's model view content
* TODO mairvine 4/18/18: use a model view content type
*/
content: string | DialogTab[],
/**
* The caption of the OK button
* The ok button
*/
okTitle: string;
okButton: Button;
/**
* The caption of the Cancel button
* The cancel button
*/
cancelTitle: string;
cancelButton: Button;
/**
* Any additional buttons that should be displayed
*/
customButtons: Button[];
/**
* Opens the dialog
*/
open(): void;
/**
* Closes the dialog
*/
close(): void;
/**
* Updates the dialog on screen to reflect changes to the buttons or content
*/
updateContent(): void;
/**
* Raised when dialog's ok button is pressed
*/
readonly onOk: vscode.Event<void>;
/**
* Raised when dialog is canceled
*/
readonly onCancel: vscode.Event<void>;
}
export interface DialogTab {
/**
* The title of the tab
*/
title: string,
title: string;
/**
* A string giving the ID of the tab's model view content
* TODO mairvine 4/18/18: use a model view content type
*/
content: string;
/**
* Updates the dialog on screen to reflect changes to the content
*/
updateContent(): void;
}
export interface Button {
/**
* The label displayed on the button
*/
label: string,
label: string;
/**
* Whether the button is enabled
*/
enabled: boolean,
enabled: boolean;
/**
* Whether the button is hidden
*/
hidden: boolean;
/**
* Raised when the button is clicked

View File

@@ -68,14 +68,17 @@ export enum ModelComponentTypes {
FlexContainer,
Card,
InputBox,
DropDown,
Button,
DashboardWidget,
DashboardWebview
DashboardWebview,
Form
}
export interface IComponentShape {
type: ModelComponentTypes;
id: string;
properties?: { [key: string]: any };
properties?: { [key: string]: any };
layout?: any;
itemConfigs?: IItemConfig[];
}
@@ -87,10 +90,30 @@ export interface IItemConfig {
export enum ComponentEventType {
PropertiesChanged,
onDidChange
onDidChange,
onDidClick
}
export interface IComponentEventArgs {
eventType: ComponentEventType;
args: any;
}
export interface IModelViewDialogDetails {
title: string;
content: string | number[];
okButton: number;
cancelButton: number;
customButtons: number[];
}
export interface IModelViewTabDetails {
title: string;
content: string;
}
export interface IModelViewButtonDetails {
label: string;
enabled: boolean;
hidden: boolean;
}

View File

@@ -33,6 +33,11 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
return new ContainerBuilderImpl<sqlops.FlexContainer, sqlops.FlexLayout, sqlops.FlexItemLayout>(this._proxy, this._handle, ModelComponentTypes.FlexContainer, id);
}
formContainer(): sqlops.FormBuilder {
let id = this.getNextComponentId();
return new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id);
}
card(): sqlops.ComponentBuilder<sqlops.CardComponent> {
let id = this.getNextComponentId();
return this.withEventHandler(new CardWrapper(this._proxy, this._handle, id), id);
@@ -43,6 +48,16 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
return this.withEventHandler(new InputBoxWrapper(this._proxy, this._handle, id), id);
}
button(): sqlops.ComponentBuilder<sqlops.ButtonComponent> {
let id = this.getNextComponentId();
return this.withEventHandler(new ButtonWrapper(this._proxy, this._handle, id), id);
}
dropDown(): sqlops.ComponentBuilder<sqlops.DropDownComponent> {
let id = this.getNextComponentId();
return this.withEventHandler(new DropDownWrapper(this._proxy, this._handle, id), id);
}
dashboardWidget(widgetId: string): sqlops.ComponentBuilder<sqlops.WidgetComponent> {
let id = this.getNextComponentId();
return this.withEventHandler<sqlops.WidgetComponent>(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id), id);
@@ -122,9 +137,40 @@ class ContainerBuilderImpl<T extends sqlops.Component, TLayout, TItemLayout> ext
}
}
class FormContainerBuilder extends ContainerBuilderImpl<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> {
withFormItems(components: sqlops.FormComponent[], itemLayout?: sqlops.FormItemLayout): sqlops.ContainerBuilder<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> {
this._component.itemConfigs = components.map(item => {
let componentWrapper = item.component as ComponentWrapper;
let actions: string[] = undefined;
if (item.actions) {
actions = item.actions.map(action => {
let actionComponentWrapper = action as ComponentWrapper;
return actionComponentWrapper.id;
});
}
return new InternalItemConfig(componentWrapper, Object.assign({}, itemLayout, {
title: item.title,
actions: actions,
isFormComponent: true
}));
});
components.forEach(formItem => {
if (formItem.actions) {
formItem.actions.forEach(component => {
let componentWrapper = component as ComponentWrapper;
this._component.itemConfigs.push(new InternalItemConfig(componentWrapper, itemLayout));
});
}
});
return this;
}
}
class InternalItemConfig {
constructor(private _component: ComponentWrapper, public config: any) {}
constructor(private _component: ComponentWrapper, public config: any) { }
public toIItemConfig(): IItemConfig {
return {
@@ -146,6 +192,7 @@ class ComponentWrapper implements sqlops.Component {
private _onErrorEmitter = new Emitter<Error>();
public readonly onError: vscode.Event<Error> = this._onErrorEmitter.event;
protected _emitterMap = new Map<ComponentEventType, Emitter<any>>();
constructor(protected readonly _proxy: MainThreadModelViewShape,
protected readonly _handle: number,
@@ -169,7 +216,7 @@ class ComponentWrapper implements sqlops.Component {
}
public toComponentShape(): IComponentShape {
return <IComponentShape> {
return <IComponentShape>{
id: this.id,
type: this.type,
layout: this.layout,
@@ -183,13 +230,13 @@ class ComponentWrapper implements sqlops.Component {
return this._proxy.$clearContainer(this._handle, this.id);
}
public addItems(items: Array<sqlops.Component>, itemLayout ?: any): void {
for(let item of items) {
public addItems(items: Array<sqlops.Component>, itemLayout?: any): void {
for (let item of items) {
this.addItem(item, itemLayout);
}
}
public addItem(item: sqlops.Component, itemLayout ?: any): void {
public addItem(item: sqlops.Component, itemLayout?: any): void {
let itemImpl = item as ComponentWrapper;
if (!itemImpl) {
throw new Error(nls.localize('unknownComponentType', 'Unkown component type. Must use ModelBuilder to create objects'));
@@ -218,7 +265,12 @@ class ComponentWrapper implements sqlops.Component {
public onEvent(eventArgs: IComponentEventArgs) {
if (eventArgs && eventArgs.eventType === ComponentEventType.PropertiesChanged) {
this.properties = eventArgs.args;
}
} else if (eventArgs) {
let emitter = this._emitterMap.get(eventArgs.eventType);
if (emitter) {
emitter.fire();
}
}
}
protected setProperty(key: string, value: any): Thenable<boolean> {
@@ -278,9 +330,6 @@ class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxCompone
this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>());
}
private _onTextChangedEmitter = new Emitter<any>();
private _emitterMap = new Map<ComponentEventType, Emitter<any>>();
public get value(): string {
return this.properties['value'];
}
@@ -292,15 +341,54 @@ class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxCompone
let emitter = this._emitterMap.get(ComponentEventType.onDidChange);
return emitter && emitter.event;
}
}
public onEvent(eventArgs: IComponentEventArgs) {
super.onEvent(eventArgs);
if (eventArgs) {
let emitter = this._emitterMap.get(eventArgs.eventType);
if (emitter) {
emitter.fire();
}
}
class DropDownWrapper extends ComponentWrapper implements sqlops.DropDownComponent {
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
super(proxy, handle, ModelComponentTypes.DropDown, id);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>());
}
public get value(): string {
return this.properties['value'];
}
public set value(v: string) {
this.setProperty('value', v);
}
public get values(): string[] {
return this.properties['values'];
}
public set values(v: string[]) {
this.setProperty('values', v);
}
public get onValueChanged(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidChange);
return emitter && emitter.event;
}
}
class ButtonWrapper extends ComponentWrapper implements sqlops.ButtonComponent {
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
super(proxy, handle, ModelComponentTypes.Button, id);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
}
public get label(): string {
return this.properties['label'];
}
public set label(v: string) {
this.setProperty('label', v);
}
public get onDidClick(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
}

View File

@@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* 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 { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import Event, { Emitter } from 'vs/base/common/event';
import { deepClone } from 'vs/base/common/objects';
import * as nls from 'vs/nls';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
class DialogImpl implements sqlops.window.modelviewdialog.Dialog {
public title: string;
public content: string | sqlops.window.modelviewdialog.DialogTab[];
public okButton: sqlops.window.modelviewdialog.Button;
public cancelButton: sqlops.window.modelviewdialog.Button;
public customButtons: sqlops.window.modelviewdialog.Button[];
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) {
this.okButton = this._extHostModelViewDialog.createButton(nls.localize('dialogOkLabel', 'Done'));
this.cancelButton = this._extHostModelViewDialog.createButton(nls.localize('dialogCancelLabel', 'Cancel'));
}
}
class TabImpl implements sqlops.window.modelviewdialog.DialogTab {
public title: string;
public content: string;
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) { }
}
class ButtonImpl implements sqlops.window.modelviewdialog.Button {
private _label: string;
private _enabled: boolean;
private _hidden: boolean;
private _onClick = new Emitter<void>();
public onClick = this._onClick.event;
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) {
this._enabled = true;
this._hidden = false;
}
public get label(): string {
return this._label;
}
public set label(label: string) {
this._label = label;
this._extHostModelViewDialog.updateButton(this);
}
public get enabled(): boolean {
return this._enabled;
}
public set enabled(enabled: boolean) {
this._enabled = enabled;
this._extHostModelViewDialog.updateButton(this);
}
public get hidden(): boolean {
return this._hidden;
}
public set hidden(hidden: boolean) {
this._hidden = hidden;
this._extHostModelViewDialog.updateButton(this);
}
public getOnClickCallback(): () => void {
return () => this._onClick.fire();
}
}
export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
private static _currentHandle = 0;
private readonly _proxy: MainThreadModelViewDialogShape;
private readonly _dialogHandles = new Map<sqlops.window.modelviewdialog.Dialog, number>();
private readonly _tabHandles = new Map<sqlops.window.modelviewdialog.DialogTab, number>();
private readonly _buttonHandles = new Map<sqlops.window.modelviewdialog.Button, number>();
private readonly _onClickCallbacks = new Map<number, () => void>();
constructor(
mainContext: IMainContext
) {
this._proxy = mainContext.getProxy(SqlMainContext.MainThreadModelViewDialog);
}
private static getNewHandle() {
let handle = ExtHostModelViewDialog._currentHandle;
ExtHostModelViewDialog._currentHandle += 1;
return handle;
}
private getDialogHandle(dialog: sqlops.window.modelviewdialog.Dialog) {
let handle = this._dialogHandles.get(dialog);
if (handle === undefined) {
handle = ExtHostModelViewDialog.getNewHandle();
this._dialogHandles.set(dialog, handle);
}
return handle;
}
private getTabHandle(tab: sqlops.window.modelviewdialog.DialogTab) {
let handle = this._tabHandles.get(tab);
if (handle === undefined) {
handle = ExtHostModelViewDialog.getNewHandle();
this._tabHandles.set(tab, handle);
}
return handle;
}
private getButtonHandle(button: sqlops.window.modelviewdialog.Button) {
let handle = this._buttonHandles.get(button);
if (handle === undefined) {
handle = ExtHostModelViewDialog.getNewHandle();
this._buttonHandles.set(button, handle);
}
return handle;
}
public $onButtonClick(handle: number): void {
this._onClickCallbacks.get(handle)();
}
public open(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getDialogHandle(dialog);
this.updateDialogContent(dialog);
this._proxy.$open(handle);
}
public close(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getDialogHandle(dialog);
this._proxy.$close(handle);
}
public updateDialogContent(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getDialogHandle(dialog);
let tabs = dialog.content;
if (tabs && typeof tabs !== 'string') {
tabs.forEach(tab => this.updateTabContent(tab));
}
if (dialog.customButtons) {
dialog.customButtons.forEach(button => this.updateButton(button));
}
this.updateButton(dialog.okButton);
this.updateButton(dialog.cancelButton);
this._proxy.$setDialogDetails(handle, {
title: dialog.title,
okButton: this.getButtonHandle(dialog.okButton),
cancelButton: this.getButtonHandle(dialog.cancelButton),
content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getTabHandle(tab)) : dialog.content as string,
customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getButtonHandle(button)) : undefined
});
}
public updateTabContent(tab: sqlops.window.modelviewdialog.DialogTab): void {
let handle = this.getTabHandle(tab);
this._proxy.$setTabDetails(handle, {
title: tab.title,
content: tab.content
});
}
public updateButton(button: sqlops.window.modelviewdialog.Button): void {
let handle = this.getButtonHandle(button);
this._proxy.$setButtonDetails(handle, {
label: button.label,
enabled: button.enabled,
hidden: button.hidden
});
}
public registerOnClickCallback(button: sqlops.window.modelviewdialog.Button, callback: () => void) {
let handle = this.getButtonHandle(button);
this._onClickCallbacks.set(handle, callback);
}
public createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
let dialog = new DialogImpl(this);
dialog.title = title;
this.getDialogHandle(dialog);
return dialog;
}
public createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
let tab = new TabImpl(this);
tab.title = title;
this.getTabHandle(tab);
return tab;
}
public createButton(label: string): sqlops.window.modelviewdialog.Button {
let button = new ButtonImpl(this);
this.getButtonHandle(button);
this.registerOnClickCallback(button, button.getOnClickCallback());
button.label = label;
return button;
}
}

View File

@@ -45,7 +45,7 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi
}
$initializeModel(handle: number, rootComponent: IComponentShape): Thenable<void> {
return this.execModelViewAction(handle, (modelView) => {
return this.execModelViewAction(handle, (modelView) => {
modelView.initializeModel(rootComponent);
});
}
@@ -67,11 +67,13 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi
this._proxy.$handleEvent(handle, componentId, eventArgs);
}
$registerEvent(handle: number, componentId: string): Thenable<void> {
$registerEvent(handle: number, componentId: string): Thenable<void> {
let properties: { [key: string]: any; } = { eventName: this.onEvent };
return this.execModelViewAction(handle, (modelView) => {
this._register(modelView.onEvent (e => {
this.onEvent(handle, componentId, e);
this._register(modelView.onEvent(e => {
if (e.componentId && e.componentId === componentId) {
this.onEvent(handle, componentId, e);
}
}));
});
}

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { MainThreadModelViewDialogShape, SqlMainContext, ExtHostModelViewDialogShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { Dialog, DialogTab, DialogButton } from 'sql/platform/dialog/dialogTypes';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { CustomDialogService } from 'sql/platform/dialog/customDialogService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
@extHostNamedCustomer(SqlMainContext.MainThreadModelViewDialog)
export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape {
private readonly _proxy: ExtHostModelViewDialogShape;
private readonly _dialogs = new Map<number, Dialog>();
private readonly _tabs = new Map<number, DialogTab>();
private readonly _buttons = new Map<number, DialogButton>();
private _dialogService: CustomDialogService;
constructor(
context: IExtHostContext,
@IInstantiationService instatiationService: IInstantiationService,
) {
this._proxy = context.getProxy(SqlExtHostContext.ExtHostModelViewDialog);
this._dialogService = new CustomDialogService(instatiationService);
}
public dispose(): void {
throw new Error('Method not implemented.');
}
public $open(handle: number): Thenable<void> {
let dialog = this.getDialog(handle);
this._dialogService.showDialog(dialog);
return Promise.resolve();
}
public $close(handle: number): Thenable<void> {
let dialog = this.getDialog(handle);
this._dialogService.closeDialog(dialog);
return Promise.resolve();
}
public $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void> {
let dialog = this._dialogs.get(handle);
if (!dialog) {
dialog = new Dialog(details.title);
let okButton = this.getButton(details.okButton);
let cancelButton = this.getButton(details.cancelButton);
dialog.okButton = okButton;
dialog.cancelButton = cancelButton;
this._dialogs.set(handle, dialog);
}
dialog.title = details.title;
if (details.content && typeof details.content !== 'string') {
dialog.content = details.content.map(tabHandle => this.getTab(tabHandle));
} else {
dialog.content = details.content as string;
}
if (details.customButtons) {
dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
}
return Promise.resolve();
}
public $setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void> {
let tab = this._tabs.get(handle);
if (!tab) {
tab = new DialogTab(details.title);
this._tabs.set(handle, tab);
}
tab.title = details.title;
tab.content = details.content;
return Promise.resolve();
}
public $setButtonDetails(handle: number, details: IModelViewButtonDetails): Thenable<void> {
let button = this._buttons.get(handle);
if (!button) {
button = new DialogButton(details.label, details.enabled);
button.hidden = details.hidden;
button.onClick(() => this.onButtonClick(handle));
this._buttons.set(handle, button);
} else {
button.label = details.label;
button.enabled = details.enabled;
button.hidden = details.hidden;
}
return Promise.resolve();
}
private getDialog(handle: number): Dialog {
let dialog = this._dialogs.get(handle);
if (!dialog) {
throw new Error('No dialog matching the given handle');
}
return dialog;
}
private getTab(handle: number): DialogTab {
let tab = this._tabs.get(handle);
if (!tab) {
throw new Error('No tab matching the given handle');
}
return tab;
}
private getButton(handle: number): DialogButton {
let button = this._buttons.get(handle);
if (!button) {
throw new Error('No button matching the given handle');
}
return button;
}
private onButtonClick(handle: number): void {
this._proxy.$onButtonClick(handle);
}
}

View File

@@ -32,6 +32,7 @@ import { ExtHostConnectionManagement } from 'sql/workbench/api/node/extHostConne
import { ExtHostDashboard } from 'sql/workbench/api/node/extHostDashboard';
import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplorer';
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog';
import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor';
export interface ISqlExtensionApiFactory {
@@ -65,6 +66,7 @@ export function createApiFactory(
const extHostWebviewWidgets = rpcProtocol.set(SqlExtHostContext.ExtHostDashboardWebviews, new ExtHostDashboardWebviews(rpcProtocol));
const extHostModelView = rpcProtocol.set(SqlExtHostContext.ExtHostModelView, new ExtHostModelView(rpcProtocol));
const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol));
const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol));
const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol));
@@ -283,10 +285,21 @@ export function createApiFactory(
};
const modelViewDialog: typeof sqlops.window.modelviewdialog = {
// TODO mairvine 4/18/18: Implement the extension layer for custom dialogs
createDialog(title: string): sqlops.window.modelviewdialog.Dialog { return undefined; },
createTab(title: string): sqlops.window.modelviewdialog.DialogTab { return undefined; },
createButton(label: string): sqlops.window.modelviewdialog.Button { return undefined; }
createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
return extHostModelViewDialog.createDialog(title);
},
createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
return extHostModelViewDialog.createTab(title);
},
createButton(label: string): sqlops.window.modelviewdialog.Button {
return extHostModelViewDialog.createButton(label);
},
openDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
return extHostModelViewDialog.open(dialog);
},
closeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
return extHostModelViewDialog.close(dialog);
}
};
const window: typeof sqlops.window = {

View File

@@ -21,6 +21,7 @@ import 'sql/workbench/api/electron-browser/mainThreadDashboard';
import 'sql/workbench/api/node/mainThreadDashboardWebview';
import 'sql/workbench/api/node/mainThreadQueryEditor';
import 'sql/workbench/api/node/mainThreadModelView';
import 'sql/workbench/api/node/mainThreadModelViewDialog';
import './mainThreadAccountManagement';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';

View File

@@ -16,7 +16,7 @@ import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import Event, { Emitter } from 'vs/base/common/event';
export abstract class ExtHostAccountManagementShape {
@@ -448,6 +448,7 @@ export const SqlMainContext = {
MainThreadDashboardWebview: createMainId<MainThreadDashboardWebviewShape>('MainThreadDashboardWebview'),
MainThreadModelView: createMainId<MainThreadModelViewShape>('MainThreadModelView'),
MainThreadDashboard: createMainId<MainThreadDashboardShape>('MainThreadDashboard'),
MainThreadModelViewDialog: createMainId<MainThreadModelViewDialogShape>('MainThreadModelViewDialog'),
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
};
@@ -464,6 +465,7 @@ export const SqlExtHostContext = {
ExtHostDashboardWebviews: createExtId<ExtHostDashboardWebviewsShape>('ExtHostDashboardWebviews'),
ExtHostModelView: createExtId<ExtHostModelViewShape>('ExtHostModelView'),
ExtHostDashboard: createExtId<ExtHostDashboardShape>('ExtHostDashboard'),
ExtHostModelViewDialog: createExtId<ExtHostModelViewDialogShape>('ExtHostModelViewDialog'),
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor')
};
@@ -543,10 +545,21 @@ export interface MainThreadObjectExplorerShape extends IDisposable {
$findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.NodeInfo[]>;
}
export interface ExtHostModelViewDialogShape {
$onButtonClick(handle: number): void;
}
export interface MainThreadModelViewDialogShape extends IDisposable {
$open(handle: number): Thenable<void>;
$close(handle: number): Thenable<void>;
$setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void>;
$setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void>;
$setButtonDetails(handle: number, details: IModelViewButtonDetails): Thenable<void>;
}
export interface ExtHostQueryEditorShape {
}
export interface MainThreadQueryEditorShape extends IDisposable {
$connect(fileUri: string, connectionId: string): Thenable<void>;
$runQuery(fileUri: string): 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 * as sqlops from 'sqlops';
import * as assert from 'assert';
import * as SharedServices from 'sql/parts/grid/services/sharedServices';
const testText = '<div>test text</div>';
suite('Grid shared services tests', () => {
test('textFormatter should encode HTML when formatting a DBCellValue object', () => {
// If I format a DBCellValue object that contains HTML
let cellValue = new SharedServices.DBCellValue();
cellValue.displayValue = testText;
cellValue.isNull = false;
let formattedHtml = SharedServices.textFormatter(undefined, undefined, cellValue, undefined, undefined);
// Then the result is HTML for a span element containing the cell value's display value as plain text
verifyFormattedHtml(formattedHtml, testText);
});
test('textFormatter should encode HTML when formatting a string', () => {
// If I format a string that contains HTML
let formattedHtml = SharedServices.textFormatter(undefined, undefined, testText, undefined, undefined);
// Then the result is HTML for a span element containing the given text as plain text
verifyFormattedHtml(formattedHtml, testText);
});
});
function verifyFormattedHtml(formattedHtml: string, expectedText: string): void {
// Create an element containing the span returned by the format call
let element = document.createElement('div');
element.innerHTML = formattedHtml;
let spanElement = element.children[0];
// Verify that the span element's text, not its innerHTML, matches the expected text
assert.equal(spanElement.textContent, testText);
assert.notEqual(spanElement.innerHTML, testText);
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes';
import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes';
import { Mock, It, Times } from 'typemoq';
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
import { DialogPane } from 'sql/platform/dialog/dialogPane';

View File

@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Mock, It, Times } from 'typemoq';
import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog';
import { MainThreadModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
'use strict';
suite('ExtHostModelViewDialog Tests', () => {
let extHostModelViewDialog: ExtHostModelViewDialog;
let mockProxy: Mock<MainThreadModelViewDialogShape>;
setup(() => {
mockProxy = Mock.ofInstance(<MainThreadModelViewDialogShape>{
$open: handle => undefined,
$close: handle => undefined,
$setDialogDetails: (handle, details) => undefined,
$setTabDetails: (handle, details) => undefined,
$setButtonDetails: (handle, details) => undefined
});
let mainContext = <IMainContext>{
getProxy: proxyType => mockProxy.object
};
extHostModelViewDialog = new ExtHostModelViewDialog(mainContext);
});
test('Creating a dialog returns a dialog with initialized ok and cancel buttons and the given title', () => {
let title = 'dialog_title';
let dialog = extHostModelViewDialog.createDialog(title);
assert.equal(dialog.title, title);
assert.equal(dialog.okButton.enabled, true);
assert.equal(dialog.cancelButton.enabled, true);
});
test('Creating a tab returns a tab with the given title', () => {
let title = 'tab_title';
let tab = extHostModelViewDialog.createTab(title);
assert.equal(tab.title, title);
});
test('Creating a button returns an enabled button with the given label', () => {
let label = 'button_label';
let button = extHostModelViewDialog.createButton(label);
assert.equal(button.label, label);
assert.equal(button.enabled, true);
});
test('Opening a dialog updates its tabs and buttons on the main thread', () => {
mockProxy.setup(x => x.$open(It.isAny()));
mockProxy.setup(x => x.$setDialogDetails(It.isAny(), It.isAny()));
mockProxy.setup(x => x.$setTabDetails(It.isAny(), It.isAny()));
mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny()));
// Create a dialog with 2 tabs and 2 custom buttons
let dialogTitle = 'dialog_title';
let dialog = extHostModelViewDialog.createDialog(dialogTitle);
let tab1Title = 'tab_1';
let tab1 = extHostModelViewDialog.createTab(tab1Title);
let tab2Title = 'tab_2';
let tab2 = extHostModelViewDialog.createTab(tab2Title);
dialog.content = [tab1, tab2];
let button1Label = 'button_1';
let button1 = extHostModelViewDialog.createButton(button1Label);
button1.enabled = false;
let button2Label = 'button_2';
let button2 = extHostModelViewDialog.createButton(button2Label);
// Open the dialog and verify that the correct main thread methods were called
extHostModelViewDialog.open(dialog);
mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => {
return details.enabled === false && details.label === button1Label;
})), Times.once());
mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => {
return details.enabled === true && details.label === button2Label;
})), Times.once());
mockProxy.verify(x => x.$setTabDetails(It.isAny(), It.is(details => {
return details.title === tab1Title;
})), Times.once());
mockProxy.verify(x => x.$setTabDetails(It.isAny(), It.is(details => {
return details.title === tab2Title;
})), Times.once());
mockProxy.verify(x => x.$setDialogDetails(It.isAny(), It.is(details => {
return details.title === dialogTitle;
})), Times.once());
mockProxy.verify(x => x.$open(It.isAny()), Times.once());
});
test('Button clicks are forwarded to the correct button', () => {
// Set up the proxy to record button handles
let handles = [];
mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny())).callback((handle, details) => handles.push(handle));
// Set up the buttons to record click events
let label1 = 'button_1';
let label2 = 'button_2';
let button1 = extHostModelViewDialog.createButton(label1);
let button2 = extHostModelViewDialog.createButton(label2);
let clickEvents = [];
button1.onClick(() => clickEvents.push(1));
button2.onClick(() => clickEvents.push(2));
extHostModelViewDialog.updateButton(button1);
extHostModelViewDialog.updateButton(button2);
// If the main thread sends some notifications that the buttons have been clicked
extHostModelViewDialog.$onButtonClick(handles[0]);
extHostModelViewDialog.$onButtonClick(handles[1]);
extHostModelViewDialog.$onButtonClick(handles[1]);
extHostModelViewDialog.$onButtonClick(handles[0]);
// Then the clicks should have been handled by the expected handlers
assert.deepEqual(clickEvents, [1, 2, 2, 1]);
});
});

View File

@@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Mock, It, Times } from 'typemoq';
import { MainThreadModelViewDialog } from 'sql/workbench/api/node/mainThreadModelViewDialog';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { IModelViewButtonDetails, IModelViewTabDetails, IModelViewDialogDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import { CustomDialogService } from 'sql/platform/dialog/customDialogService';
import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes';
import { ExtHostModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { Emitter } from 'vs/base/common/event';
'use strict';
suite('MainThreadModelViewDialog Tests', () => {
let mainThreadModelViewDialog: MainThreadModelViewDialog;
let mockExtHostModelViewDialog: Mock<ExtHostModelViewDialogShape>;
let mockDialogService: Mock<CustomDialogService>;
let openedDialog: Dialog;
// Dialog details
let button1Details: IModelViewButtonDetails;
let button2Details: IModelViewButtonDetails;
let okButtonDetails: IModelViewButtonDetails;
let cancelButtonDetails: IModelViewButtonDetails;
let tab1Details: IModelViewTabDetails;
let tab2Details: IModelViewTabDetails;
let dialogDetails: IModelViewDialogDetails;
let button1Handle = 1;
let button2Handle = 2;
let okButtonHandle = 3;
let cancelButtonHandle = 4;
let tab1Handle = 5;
let tab2Handle = 6;
let dialogHandle = 7;
setup(() => {
mockExtHostModelViewDialog = Mock.ofInstance(<ExtHostModelViewDialogShape>{
$onButtonClick: handle => undefined
});
let extHostContext = <IExtHostContext>{
getProxy: proxyType => mockExtHostModelViewDialog.object
};
mainThreadModelViewDialog = new MainThreadModelViewDialog(extHostContext, undefined);
// Set up the mock dialog service
mockDialogService = Mock.ofType(CustomDialogService, undefined, undefined);
openedDialog = undefined;
mockDialogService.setup(x => x.showDialog(It.isAny())).callback(dialog => openedDialog = dialog);
(mainThreadModelViewDialog as any)._dialogService = mockDialogService.object;
// Set up the dialog details
button1Details = {
label: 'button1',
enabled: false,
hidden: false
};
button2Details = {
label: 'button2',
enabled: true,
hidden: false
};
okButtonDetails = {
label: 'ok_label',
enabled: true,
hidden: false
};
cancelButtonDetails = {
label: 'cancel_label',
enabled: true,
hidden: false
};
tab1Details = {
title: 'tab1',
content: 'content1'
};
tab2Details = {
title: 'tab2',
content: 'content2'
};
dialogDetails = {
title: 'dialog1',
content: [tab1Handle, tab2Handle],
okButton: okButtonHandle,
cancelButton: cancelButtonHandle,
customButtons: [button1Handle, button2Handle]
};
// Register the buttons, tabs, and dialog
mainThreadModelViewDialog.$setButtonDetails(button1Handle, button1Details);
mainThreadModelViewDialog.$setButtonDetails(button2Handle, button2Details);
mainThreadModelViewDialog.$setButtonDetails(okButtonHandle, okButtonDetails);
mainThreadModelViewDialog.$setButtonDetails(cancelButtonHandle, cancelButtonDetails);
mainThreadModelViewDialog.$setTabDetails(tab1Handle, tab1Details);
mainThreadModelViewDialog.$setTabDetails(tab2Handle, tab2Details);
mainThreadModelViewDialog.$setDialogDetails(dialogHandle, dialogDetails);
});
test('Creating a dialog and calling open on it causes a dialog with correct content and buttons to open', () => {
// If I open the dialog
mainThreadModelViewDialog.$open(dialogHandle);
// Then the opened dialog's content and buttons match what was set
mockDialogService.verify(x => x.showDialog(It.isAny()), Times.once());
assert.notEqual(openedDialog, undefined);
assert.equal(openedDialog.title, dialogDetails.title);
assert.equal(openedDialog.okButton.label, okButtonDetails.label);
assert.equal(openedDialog.okButton.enabled, okButtonDetails.enabled);
assert.equal(openedDialog.cancelButton.label, cancelButtonDetails.label);
assert.equal(openedDialog.cancelButton.enabled, cancelButtonDetails.enabled);
assert.equal(openedDialog.customButtons.length, 2);
assert.equal(openedDialog.customButtons[0].label, button1Details.label);
assert.equal(openedDialog.customButtons[0].enabled, button1Details.enabled);
assert.equal(openedDialog.customButtons[1].label, button2Details.label);
assert.equal(openedDialog.customButtons[1].enabled, button2Details.enabled);
assert.equal(openedDialog.content.length, 2);
assert.equal((openedDialog.content[0] as DialogTab).content, tab1Details.content);
assert.equal((openedDialog.content[0] as DialogTab).title, tab1Details.title);
assert.equal((openedDialog.content[1] as DialogTab).content, tab2Details.content);
assert.equal((openedDialog.content[1] as DialogTab).title, tab2Details.title);
});
test('Button presses are forwarded to the extension host', () => {
// Set up the mock proxy to capture button presses
let pressedHandles = [];
mockExtHostModelViewDialog.setup(x => x.$onButtonClick(It.isAny())).callback(handle => pressedHandles.push(handle));
// Open the dialog so that its buttons can be accessed
mainThreadModelViewDialog.$open(dialogHandle);
// Set up click emitters for each button
let okEmitter = new Emitter<void>();
let cancelEmitter = new Emitter<void>();
let button1Emitter = new Emitter<void>();
let button2Emitter = new Emitter<void>();
openedDialog.okButton.registerClickEvent(okEmitter.event);
openedDialog.cancelButton.registerClickEvent(cancelEmitter.event);
openedDialog.customButtons[0].registerClickEvent(button1Emitter.event);
openedDialog.customButtons[1].registerClickEvent(button2Emitter.event);
// Click the buttons
button1Emitter.fire();
button2Emitter.fire();
okEmitter.fire();
cancelEmitter.fire();
button2Emitter.fire();
cancelEmitter.fire();
button1Emitter.fire();
okEmitter.fire();
// Verify that the correct button click notifications were sent to the proxy
assert.deepEqual(pressedHandles, [button1Handle, button2Handle, okButtonHandle, cancelButtonHandle, button2Handle, cancelButtonHandle, button1Handle, okButtonHandle]);
});
});

View File

@@ -462,11 +462,12 @@ export class IssueReporter extends Disposable {
response.json().then(result => {
this.clearSearchResults();
if (result && result.candidates) {
this.displaySearchResults(result.candidates);
} else {
throw new Error('Unexpected response, no candidates property');
}
// {{SQL CARBON EDIT}}
// if (result && result.candidates) {
// this.displaySearchResults(result.candidates);
// } else {
// throw new Error('Unexpected response, no candidates property');
// }
}).catch((error) => {
this.logSearchError(error);
});

View File

@@ -59,13 +59,14 @@ export class IssueReporterModel {
assign(this._data, newData);
}
// {{SQL CARBON EDIT}}
serialize(): string {
return `
Issue Type: <b>${this.getIssueTypeTitle()}</b>
${this._data.issueDescription}
VS Code version: ${this._data.versionInfo && this._data.versionInfo.vscodeVersion}
SQL Operations Studio version: ${this._data.versionInfo && this._data.versionInfo.vscodeVersion}
OS version: ${this._data.versionInfo && this._data.versionInfo.os}
${this.getInfos()}

View File

@@ -23,6 +23,7 @@ suite('IssueReporter', () => {
});
});
// {{SQL CARBON EDIT}}
test('serializes model skeleton when no data is provided', () => {
const issueReporterModel = new IssueReporterModel();
assert.equal(issueReporterModel.serialize(),
@@ -31,7 +32,7 @@ Issue Type: <b>Feature Request</b>
undefined
VS Code version: undefined
SQL Operations Studio version: undefined
OS version: undefined

View File

@@ -435,7 +435,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
return this.queryGallery(query).then(({ galleryExtensions, total }) => {
const extensions = galleryExtensions.map((e, index) => toExtension(e, this.extensionsGalleryUrl, index, query, options.source));
const pageSize = query.pageSize;
// {{SQL CARBON EDIT}}
const pageSize = extensions.length;
const getPage = (pageIndex: number) => {
const nextPageQuery = query.withPage(pageIndex + 1);
return this.queryGallery(nextPageQuery)

View File

@@ -31,7 +31,18 @@ function getClient(aiKey: string): typeof appInsights.client {
const client = appInsights.getClient(aiKey);
client.channel.setOfflineMode(true);
client.context.tags[client.context.keys.deviceMachineName] = ''; //prevent App Insights from reporting machine name
// {{SQL CARBON EDIT}}
// clear all ID fields from telemetry
client.context.tags[client.context.keys.deviceMachineName] = '';
client.context.tags[client.context.keys.cloudRoleInstance] = '';
// set envelope flags to suppress Vortex ingest header
client.addTelemetryProcessor((envelope, contextObjects) => {
envelope.flags = 0x200000;
return true;
});
if (aiKey.indexOf('AIF-') === 0) {
client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1';
}

View File

@@ -14,10 +14,15 @@ import product from 'vs/platform/node/product';
export function resolveCommonProperties(commit: string, version: string, machineId: string, installSourcePath: string): TPromise<{ [name: string]: string; }> {
const result: { [name: string]: string; } = Object.create(null);
// {{SQL CARBON EDIT}}
// __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
result['common.machineId'] = machineId;
// __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['sessionID'] = uuid.generateUuid() + Date.now();
// result['common.machineId'] = machineId;
result['common.machineId'] = '';
// // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// result['sessionID'] = uuid.generateUuid() + Date.now();
result['sessionID'] = '';
// __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['commitHash'] = commit;
// __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
@@ -32,7 +37,7 @@ export function resolveCommonProperties(commit: string, version: string, machine
result['common.nodePlatform'] = process.platform;
// __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.nodeArch'] = process.arch;
// {{SQL CARBON EDIT}}
result['common.application.name'] = product.nameLong;

View File

@@ -20,48 +20,34 @@ export function resolveWorkbenchCommonProperties(storageService: IStorageService
result['common.version.renderer'] = process.versions && (<any>process).versions['chrome'];
// {{SQL CARBON EDIT}}
result['common.application.name'] = product.nameLong;
getUserId(storageService).then(value => result['common.userId'] = value);
// {{SQL CARBON EDIT}}
result['common.userId'] = '';
const lastSessionDate = storageService.get('telemetry.lastSessionDate');
const firstSessionDate = storageService.get('telemetry.firstSessionDate') || new Date().toUTCString();
storageService.store('telemetry.firstSessionDate', firstSessionDate);
storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
// {{SQL CARBON EDIT}}
// const lastSessionDate = storageService.get('telemetry.lastSessionDate');
// const firstSessionDate = storageService.get('telemetry.firstSessionDate') || new Date().toUTCString();
// storageService.store('telemetry.firstSessionDate', firstSessionDate);
// storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
// __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.firstSessionDate'] = firstSessionDate;
// __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.lastSessionDate'] = lastSessionDate;
// __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.isNewSession'] = !lastSessionDate ? '1' : '0';
// // __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// result['common.firstSessionDate'] = firstSessionDate;
// // __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// result['common.lastSessionDate'] = lastSessionDate;
// // __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// result['common.isNewSession'] = !lastSessionDate ? '1' : '0';
// {{SQL CARBON EDIT}}
// __GDPR__COMMON__ "common.instanceId" : { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
result['common.instanceId'] = getOrCreateInstanceId(storageService);
// result['common.instanceId'] = getOrCreateInstanceId(storageService);
result['common.instanceId'] = '';
return result;
});
}
function getOrCreateInstanceId(storageService: IStorageService): string {
const result = storageService.get('telemetry.instanceId') || uuid.generateUuid();
storageService.store('telemetry.instanceId', result);
return result;
}
// {{SQL CARBON EDIT}}
// Get the unique ID for the current user
function getUserId(storageService: IStorageService): Promise<string> {
var userId = storageService.get('common.userId');
return new Promise<string>(resolve => {
// Generate the user id if it has not been created already
if (typeof userId === 'undefined') {
let id = Utils.generateUserId();
id.then( newId => {
userId = newId;
resolve(userId);
//store the user Id in the storage service
storageService.store('common.userId', userId);
});
} else {
resolve(userId);
}
});
}
// function getOrCreateInstanceId(storageService: IStorageService): string {
// const result = storageService.get('telemetry.instanceId') || uuid.generateUuid();
// storageService.store('telemetry.instanceId', result);
// return result;
// }

View File

@@ -32,58 +32,59 @@ suite('Telemetry - common properties', function () {
del(parentDir, os.tmpdir(), done);
});
test('default', function () {
return mkdirp(parentDir).then(() => {
fs.writeFileSync(installSource, 'my.install.source');
// {{SQL CARBON EDIT}}
// test('default', function () {
// return mkdirp(parentDir).then(() => {
// fs.writeFileSync(installSource, 'my.install.source');
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
assert.ok('commitHash' in props);
assert.ok('sessionID' in props);
assert.ok('timestamp' in props);
assert.ok('common.platform' in props);
assert.ok('common.nodePlatform' in props);
assert.ok('common.nodeArch' in props);
assert.ok('common.timesincesessionstart' in props);
assert.ok('common.sequence' in props);
// return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
// assert.ok('commitHash' in props);
// assert.ok('sessionID' in props);
// assert.ok('timestamp' in props);
// assert.ok('common.platform' in props);
// assert.ok('common.nodePlatform' in props);
// assert.ok('common.nodeArch' in props);
// assert.ok('common.timesincesessionstart' in props);
// assert.ok('common.sequence' in props);
// assert.ok('common.version.shell' in first.data); // only when running on electron
// assert.ok('common.version.renderer' in first.data);
assert.ok('common.osVersion' in props, 'osVersion');
assert.ok('common.platformVersion' in props, 'platformVersion');
assert.ok('version' in props);
assert.equal(props['common.source'], 'my.install.source');
// // assert.ok('common.version.shell' in first.data); // only when running on electron
// // assert.ok('common.version.renderer' in first.data);
// assert.ok('common.osVersion' in props, 'osVersion');
// assert.ok('common.platformVersion' in props, 'platformVersion');
// assert.ok('version' in props);
// assert.equal(props['common.source'], 'my.install.source');
// {{SQL CARBON EDIT}}
assert.ok('common.application.name' in props);
// // {{SQL CARBON EDIT}}
// assert.ok('common.application.name' in props);
assert.ok('common.firstSessionDate' in props, 'firstSessionDate');
assert.ok('common.lastSessionDate' in props, 'lastSessionDate'); // conditional, see below, 'lastSessionDate'ow
assert.ok('common.isNewSession' in props, 'isNewSession');
// assert.ok('common.firstSessionDate' in props, 'firstSessionDate');
// assert.ok('common.lastSessionDate' in props, 'lastSessionDate'); // conditional, see below, 'lastSessionDate'ow
// assert.ok('common.isNewSession' in props, 'isNewSession');
// machine id et al
assert.ok('common.instanceId' in props, 'instanceId');
assert.ok('common.machineId' in props, 'machineId');
// // machine id et al
// assert.ok('common.instanceId' in props, 'instanceId');
// assert.ok('common.machineId' in props, 'machineId');
fs.unlinkSync(installSource);
// fs.unlinkSync(installSource);
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
assert.ok(!('common.source' in props));
});
});
});
});
// return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
// assert.ok(!('common.source' in props));
// });
// });
// });
// });
test('lastSessionDate when aviablale', function () {
// test('lastSessionDate when aviablale', function () {
storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
// storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
// return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
assert.ok('common.lastSessionDate' in props); // conditional, see below
assert.ok('common.isNewSession' in props);
assert.equal(props['common.isNewSession'], 0);
});
});
// assert.ok('common.lastSessionDate' in props); // conditional, see below
// assert.ok('common.isNewSession' in props);
// assert.equal(props['common.isNewSession'], 0);
// });
// });
test('values chance on ask', function () {
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {

View File

@@ -177,7 +177,8 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens
private createDefaultRecommendedExtensionsListViewDescriptor(): IViewDescriptor {
return {
id: 'extensions.recommendedList',
name: localize('recommendedExtensions', "Recommended"),
// {{ SQL CARBON EDIT}}
name: localize('recommendedExtensions', "Marketplace"),
location: ViewLocation.Extensions,
ctor: RecommendedExtensionsView,
when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')),

View File

@@ -234,6 +234,9 @@ export class ExtensionsListView extends ViewsViewletPanel {
return this.getAllRecommendationsModel(query, options);
} else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
return this.getRecommendationsModel(query, options);
// {{SQL CARBON EDIT}}
} else if (ExtensionsListView.isAllMarketplaceExtensionsQuery(query.value)) {
return this.getAllMarketplaceModel(query, options);
}
let text = query.value;
@@ -363,6 +366,42 @@ export class ExtensionsListView extends ViewsViewletPanel {
});
}
// {{SQL CARBON EDIT}}
private getAllMarketplaceModel(query: Query, options: IQueryOptions): TPromise<IPagedModel<IExtension>> {
const value = query.value.trim().toLowerCase();
return this.extensionsWorkbenchService.queryLocal()
.then(result => result.filter(e => e.type === LocalExtensionType.User))
.then(local => {
return this.tipsService.getOtherRecommendations().then((recommmended) => {
const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
options = assign(options, { text: value, source: 'searchText' });
return TPromise.as(this.extensionsWorkbenchService.queryGallery(options).then((pager) => {
// filter out installed extensions
pager.firstPage = pager.firstPage.filter((p) => {
return installedExtensions.indexOf(`${p.publisher}.${p.name}`) === -1;
});
// sort the marketplace extensions
pager.firstPage.sort((a, b) => {
let isRecommendedA: boolean = recommmended.indexOf(`${a.publisher}.${a.name}`) > -1;
let isRecommendedB: boolean = recommmended.indexOf(`${b.publisher}.${b.name}`) > -1;
// sort recommeded extensions before other extensions
if (isRecommendedA !== isRecommendedB) {
return (isRecommendedA && !isRecommendedB) ? -1 : 1;
}
// otherwise sort by name
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1;
});
pager.total = pager.firstPage.length;
pager.pageSize = pager.firstPage.length;
return new PagedModel(pager || []);
}));
});
});
}
// Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions
private getTrimmedRecommendations(installedExtensions: string[], value: string, fileBasedRecommendations: string[], otherRecommendations: string[], workpsaceRecommendations: string[], ) {
const totalCount = 8;
@@ -524,6 +563,11 @@ export class ExtensionsListView extends ViewsViewletPanel {
static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
return /@recommended:keymaps/i.test(query);
}
// {{SQL CARBON EDIT}}
static isAllMarketplaceExtensionsQuery(query: string): boolean {
return /@allmarketplace/i.test(query);
}
}
export class InstalledExtensionsView extends ExtensionsListView {
@@ -560,7 +604,8 @@ export class BuiltInExtensionsView extends ExtensionsListView {
export class RecommendedExtensionsView extends ExtensionsListView {
async show(query: string): TPromise<IPagedModel<IExtension>> {
return super.show(!query.trim() ? '@recommended:all' : '@recommended');
// {{SQL CARBON EDIT}}
return super.show('@allmarketplace');
}
}