Initial Code Layering (#3788)

* working on formatting

* fixed basic lint errors; starting moving things to their appropriate location

* formatting

* update tslint to match the version of vscode we have

* remove unused code

* work in progress fixing layering

* formatting

* moved connection management service to platform

* formatting

* add missing file

* moving more servies

* formatting

* moving more services

* formatting

* wip

* moving more services

* formatting

* revert back tslint rules

* move css file

* add missing svgs
This commit is contained in:
Anthony Dresser
2019-01-25 14:52:35 -08:00
committed by GitHub
parent c8986464ec
commit ea67859de7
338 changed files with 2036 additions and 7386 deletions

View File

@@ -10,8 +10,8 @@ import { Subject } from 'rxjs/Subject';
import { Observer } from 'rxjs/Observer';
import { ResultSetSubset, EditUpdateCellResult, EditSubsetResult, EditCreateRowResult } from 'sqlops';
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
import { ResultSerializer } from 'sql/parts/query/common/resultSerializer';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { ResultSerializer } from 'sql/platform/node/resultSerializer';
import { ISaveRequest } from 'sql/parts/grid/common/interfaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';

View File

@@ -6,14 +6,14 @@
'use strict';
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
import { EditDataEditor } from 'sql/parts/editData/editor/editDataEditor';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
function runActionOnActiveResultsEditor (accessor: ServicesAccessor, eventName: string): void {
function runActionOnActiveResultsEditor(accessor: ServicesAccessor, eventName: string): void {
let editorService = accessor.get(IEditorService);
const candidates = [editorService.activeControl, ...editorService.visibleControls].filter(e => {
if (e) {
@@ -28,7 +28,7 @@ function runActionOnActiveResultsEditor (accessor: ServicesAccessor, eventName:
if (candidates.length > 0) {
let queryModelService: IQueryModelService = accessor.get(IQueryModelService);
let uri = (<any> candidates[0].input).uri;
let uri = (<any>candidates[0].input).uri;
queryModelService.sendGridContentEvent(uri, eventName);
}
}

View File

@@ -14,11 +14,11 @@ import 'vs/css!sql/parts/grid/media/slickGrid';
import { Subscription, Subject } from 'rxjs/Rx';
import { ElementRef, QueryList, ChangeDetectorRef, ViewChildren } from '@angular/core';
import { SlickGrid } from 'angular2-slickgrid';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
import * as Constants from 'sql/parts/query/common/constants';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import { IGridInfo, IGridDataSet, SaveFormat } from 'sql/parts/grid/common/interfaces';
import * as Utils from 'sql/parts/connection/common/utils';
import * as Utils from 'sql/platform/connection/common/utils';
import { DataService } from 'sql/parts/grid/services/dataService';
import * as actions from 'sql/parts/grid/views/gridActions';
import * as Services from 'sql/parts/grid/services/sharedServices';
@@ -235,7 +235,7 @@ export abstract class GridParentComponent {
let selection = this.slickgrids.toArray()[index || this.activeGrid].getSelectedRanges();
if (selection) {
selection = selection.map(c => { return <Slick.Range>{ fromCell: c.fromCell - 1, toCell: c.toCell - 1, toRow: c.toRow, fromRow: c.fromRow }; });
return selection;
return selection;
} else {
return undefined;
}

View File

@@ -1,261 +0,0 @@
<div #taskbarContainer></div>
<div style="display: flex; flex-flow: row; overflow: hidden; height: 100%; width: 100%">
<div style="flex:3 3 auto; margin: 5px; overflow: scroll">
<div style="position: relative; width: calc(100% - 20px); height: calc(100% - 20px)">
<ng-template component-host></ng-template>
</div>
</div>
<div class="angular-modal-body-content chart-viewer" style="flex:1 1 auto; border-left: 1px solid; margin: 5px;">
<div style="position: relative; width: 100%">
<div class="dialog-label">{{localizedStrings.CHART_TYPE}}</div>
<select-box #chartTypeSelect class="input-divider"
[aria-label]="localizedStrings.CHART_TYPE"
[options]="insightRegistry.getAllIds()"
[selectedOption]="getDefaultChartType()"
[onlyEmitOnChange]="true"
(onDidSelect)="onChartChanged($event)"></select-box>
<ng-container [ngSwitch]="chartTypesSelectBox.value">
<ng-container *ngSwitchCase="'line'">
<ng-container *ngTemplateOutlet="lineInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'scatter'">
<ng-container *ngTemplateOutlet="scatterInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'timeSeries'">
<ng-container *ngTemplateOutlet="scatterInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'bar'">
<ng-container *ngTemplateOutlet="barInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'horizontalBar'">
<ng-container *ngTemplateOutlet="horizontalBarInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'pie'">
<ng-container *ngTemplateOutlet="pieInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'doughnut'">
<ng-container *ngTemplateOutlet="pieInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'count'">
<ng-container *ngTemplateOutlet="countInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'image'">
<ng-container *ngTemplateOutlet="imageInput"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'table'">
<ng-container *ngTemplateOutlet="tableInput"></ng-container>
</ng-container>
</ng-container>
</div>
</div>
</div>
<!--
count control interface
-->
<ng-template #countInput>
</ng-template>
<!--
image control interface
-->
<ng-template #imageInput>
</ng-template>
<!--
table control interface
-->
<ng-template #tableInput>
</ng-template>
<!--
Line graph control interface
-->
<ng-template #lineInput>
<ng-container *ngTemplateOutlet="dataTypeInput"></ng-container>
<ng-template [ngIf]="showColumnsAsLabels">
<ng-container *ngTemplateOutlet="columnsAsLabelsInput"></ng-container>
</ng-template>
<ng-template [ngIf]="showLabelFirstColumn">
<ng-container *ngTemplateOutlet="labelFirstColumnInput"></ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="yAxisLabelInput"></ng-container>
<ng-container *ngTemplateOutlet="xAxisLabelInput"></ng-container>
<ng-container *ngTemplateOutlet="legendInput"></ng-container>
</ng-template>
<!--
scatter graph control interface
-->
<ng-template #scatterInput>
<ng-container *ngTemplateOutlet="legendInput"></ng-container>
<ng-container *ngTemplateOutlet="yAxisLabelInput"></ng-container>
<ng-container *ngTemplateOutlet="xAxisLabelInput"></ng-container>
</ng-template>
<!--
bar graph control interface
-->
<ng-template #barInput>
<ng-container *ngTemplateOutlet="dataDirectionInput"></ng-container>
<ng-template [ngIf]="showColumnsAsLabels">
<ng-container *ngTemplateOutlet="columnsAsLabelsInput"></ng-container>
</ng-template>
<ng-template [ngIf]="showLabelFirstColumn">
<ng-container *ngTemplateOutlet="labelFirstColumnInput"></ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="legendInput"></ng-container>
<ng-container *ngTemplateOutlet="yAxisLabelInput"></ng-container>
<ng-container *ngTemplateOutlet="yAxisMinMaxInput"></ng-container>
<ng-container *ngTemplateOutlet="xAxisLabelInput"></ng-container>
</ng-template>
<!--
Horizontal bar graph control interface
-->
<ng-template #horizontalBarInput>
<ng-container *ngTemplateOutlet="dataDirectionInput"></ng-container>
<ng-template [ngIf]="showColumnsAsLabels">
<ng-container *ngTemplateOutlet="columnsAsLabelsInput"></ng-container>
</ng-template>
<ng-template [ngIf]="showLabelFirstColumn">
<ng-container *ngTemplateOutlet="labelFirstColumnInput"></ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="legendInput"></ng-container>
<ng-container *ngTemplateOutlet="xAxisLabelInput"></ng-container>
<ng-container *ngTemplateOutlet="xAxisMinMaxInput"></ng-container>
<ng-container *ngTemplateOutlet="yAxisLabelInput"></ng-container>
</ng-template>
<!--
Pie graph control interface
-->
<ng-template #pieInput>
<ng-container *ngTemplateOutlet="dataDirectionInput"></ng-container>
<ng-template [ngIf]="showColumnsAsLabels">
<ng-container *ngTemplateOutlet="columnsAsLabelsInput"></ng-container>
</ng-template>
<ng-template [ngIf]="showLabelFirstColumn">
<ng-container *ngTemplateOutlet="labelFirstColumnInput"></ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="legendInput"></ng-container>
</ng-template>
<!--
Legend Input Control interface
Valid for any charting type, i.e not image/count
-->
<ng-template #legendInput>
<div class="dialog-label" id="legendLabel">{{localizedStrings.LEGEND}}</div>
<select-box class="input-divider"
[aria-label]="localizedStrings.LEGEND"
[options]="legendOptions"
[selectedOption]="_chartConfig.legendPosition"
[onlyEmitOnChange]="true"
(onDidSelect)="setConfigValue('legendPosition', $event.selected)"></select-box>
</ng-template>
<!--
Data type input control interface
point vs number data type, only valid for the line chart type
-->
<ng-template #dataTypeInput>
<div class="dialog-label" id="dataTypeLabel">{{localizedStrings.DATA_TYPE}}</div>
<div class="radio-indent" role="radiogroup" aria-labelledby="dataTypeLabel">
<div class="option">
<input type="radio" role="radio" name="data-type" value="number" [(ngModel)]="dataType" aria-labelledby="numberLabel"><span id="numberLabel">{{localizedStrings.NUMBER}}</span>
</div>
<div class="option">
<input type="radio" role="radio" name="data-type" value="point" [(ngModel)]="dataType" aria-labelledby="pointLabel"><span id="pointLabel">{{localizedStrings.POINT}}</span>
</div>
</div>
</ng-template>
<!--
Data direction input control
vertical vs horizontal, change which direction is considered a "series" of data
only valid for pie, bar, doughnut charts and line is numbr is the data type
otherwise the direction is assumed for each other type
-->
<ng-template #dataDirectionInput>
<div class="dialog-label" id="dataDirectionLabel">{{localizedStrings.DATA_DIRECTION}}</div>
<div class="radio-indent" role="radiogroup" aria-labelledby="dataDirectionLabel">
<div class="option">
<input type="radio" role="radio" name="data-direction" value="vertical" [(ngModel)]="dataDirection" aria-labelledby="verticalLabel"><span id="verticalLabel">{{localizedStrings.VERTICAL}}</span>
</div>
<div class="option">
<input type="radio" role="radio" name="data-direction" value="horizontal" [(ngModel)]="dataDirection" aria-labelledby="horizontalLabel"><span id="horizontalLabel">{{localizedStrings.HORIZONTAL}}</span>
</div>
</div>
</ng-template>
<!--
Control for specifing if the first column should be used to label the series
Only valid for data direction = horizontal
-->
<ng-template #labelFirstColumnInput>
<checkbox class="input-divider" [label]="localizedStrings.LABEL_FIRST_COLUMN" (onChange)="setConfigValue('labelFirstColumn', $event)"></checkbox>
</ng-template>
<!--
Control whether to use column names as series labels
Only valid for data direction = vertical
-->
<ng-template #columnsAsLabelsInput>
<checkbox class="input-divider" [label]="localizedStrings.COLUMNS_AS_LABELS" (onChange)="setConfigValue('columnsAsLabels', $event)"></checkbox>
</ng-template>
<!--
Y-Axis options controls
valid for any charting value
-->
<ng-template #yAxisLabelInput>
<span>
{{localizedStrings.Y_AXIS_LABEL}}
<input-box (onDidChange)="setConfigValue('yAxisLabel', $event)"
[aria-label]="localizedStrings.Y_AXIS_LABEL"></input-box>
</span>
</ng-template>
<ng-template #yAxisMinMaxInput>
<span>
{{localizedStrings.Y_AXIS_MAX_VAL}}
<input-box [type]="'number'"
(onDidChange)="setConfigValue('yAxisMax', $event)"
[aria-label]="localizedStrings.Y_AXIS_MAX_VAL"></input-box>
</span>
<span>
{{localizedStrings.Y_AXIS_MIN_VAL}}
<input-box [type]="'number'"
(onDidChange)="setConfigValue('yAxisMin', $event)"
[aria-label]="localizedStrings.Y_AXIS_MIN_VAL"></input-box>
</span>
</ng-template>
<!--
X-Axis options controls
valid for any charting value
-->
<ng-template #xAxisLabelInput>
<span>
{{localizedStrings.X_AXIS_LABEL}}
<input-box (onDidChange)="setConfigValue('xAxisLabel', $event)"
[aria-label]="localizedStrings.X_AXIS_LABEL"></input-box>
</span>
</ng-template>
<ng-template #xAxisMinMaxInput>
<span>
{{localizedStrings.X_AXIS_MAX_VAL}}
<input-box [type]="'number'"
(onDidChange)="setConfigValue('xAxisMax', $event)"
[aria-label]="localizedStrings.X_AXIS_MAX_VAL"></input-box>
</span>
<span>
{{localizedStrings.X_AXIS_MIN_VAL}}
<input-box [type]="'number'"
(onDidChange)="setConfigValue('xAxisMin', $event)"
[aria-label]="localizedStrings.X_AXIS_MIN_VAL"></input-box>
</span>
</ng-template>

View File

@@ -1,363 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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!sql/parts/grid/views/query/chartViewer';
import {
Component, Inject, forwardRef, OnInit, ComponentFactoryResolver, ViewChild,
OnDestroy, Input, ElementRef, ChangeDetectorRef
} from '@angular/core';
import { NgGridItemConfig } from 'angular2-grid';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
import { IInsightData, IInsightsView, IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
import { ILineConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component';
import * as PathUtilities from 'sql/common/pathUtilities';
import { IChartViewActionContext, CopyAction, CreateInsightAction, SaveImageAction } from 'sql/parts/grid/views/query/chartViewerActions';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import * as Constants from 'sql/parts/query/common/constants';
import { SelectBox as AngularSelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component';
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { LegendPosition, DataDirection, DataType } from 'sql/parts/dashboard/widgets/insights/views/charts/interfaces';
/* Insights */
import {
ChartInsight
} from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { IDisposable } from 'vs/base/common/lifecycle';
import Severity from 'vs/base/common/severity';
import URI from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { mixin } from 'vs/base/common/objects';
import * as paths from 'vs/base/common/paths';
import * as pfs from 'vs/base/node/pfs';
import { ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
const LocalizedStrings = {
CHART_TYPE: nls.localize('chartTypeLabel', 'Chart Type'),
DATA_DIRECTION: nls.localize('dataDirectionLabel', 'Data Direction'),
VERTICAL: nls.localize('verticalLabel', 'Vertical'),
HORIZONTAL: nls.localize('horizontalLabel', 'Horizontal'),
DATA_TYPE: nls.localize('dataTypeLabel', 'Data Type'),
NUMBER: nls.localize('numberLabel', 'Number'),
POINT: nls.localize('pointLabel', 'Point'),
LABEL_FIRST_COLUMN: nls.localize('labelFirstColumnLabel', 'Use first column as row label'),
COLUMNS_AS_LABELS: nls.localize('columnsAsLabelsLabel', 'Use column names as labels'),
LEGEND: nls.localize('legendLabel', 'Legend Position'),
CHART_NOT_FOUND: nls.localize('chartNotFound', 'Could not find chart to save'),
X_AXIS_LABEL: nls.localize('xAxisLabel', 'X Axis Label'),
X_AXIS_MIN_VAL: nls.localize('xAxisMinVal', 'X Axis Minimum Value'),
X_AXIS_MAX_VAL: nls.localize('xAxisMaxVal', 'X Axis Maximum Value'),
Y_AXIS_LABEL: nls.localize('yAxisLabel', 'Y Axis Label'),
Y_AXIS_MIN_VAL: nls.localize('yAxisMinVal', 'Y Axis Minimum Value'),
Y_AXIS_MAX_VAL: nls.localize('yAxisMaxVal', 'Y Axis Maximum Value')
};
@Component({
selector: 'chart-viewer',
templateUrl: decodeURI(require.toUrl('sql/parts/grid/views/query/chartViewer.component.html'))
})
export class ChartViewerComponent implements OnInit, OnDestroy, IChartViewActionContext {
public legendOptions: string[];
@ViewChild('chartTypeSelect') private chartTypesSelectBox: AngularSelectBox;
/* UI */
private _actionBar: Taskbar;
private _createInsightAction: CreateInsightAction;
private _copyAction: CopyAction;
private _saveAction: SaveImageAction;
private _chartConfig: ILineConfig;
private _disposables: Array<IDisposable> = [];
private _executeResult: IInsightData;
private _chartComponent: ChartInsight;
protected localizedStrings = LocalizedStrings;
protected insightRegistry = insightRegistry;
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
@ViewChild('taskbarContainer', { read: ElementRef }) private taskbarContainer;
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(INotificationService) private notificationService: INotificationService,
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IClipboardService) private clipboardService: IClipboardService,
@Inject(IConfigurationService) private configurationService: IConfigurationService,
@Inject(IWindowsService) private windowsService: IWindowsService,
@Inject(IWorkspaceContextService) private workspaceContextService: IWorkspaceContextService,
@Inject(IWindowService) private windowService: IWindowService,
@Inject(IQueryModelService) private queryModelService: IQueryModelService,
@Inject(IEditorService) private editorService: IEditorService
) {
this.setDefaultChartConfig();
}
ngOnInit() {
this.legendOptions = Object.values(LegendPosition);
this._initActionBar();
}
private setDefaultChartConfig() {
let defaultChart = this.getDefaultChartType();
if (defaultChart === 'timeSeries') {
this._chartConfig = <ILineConfig>{
dataDirection: 'vertical',
dataType: 'point',
legendPosition: 'none'
};
} else {
this._chartConfig = <ILineConfig>{
dataDirection: 'vertical',
dataType: 'number',
legendPosition: 'none'
};
}
}
protected getDefaultChartType(): string {
let defaultChartType = Constants.chartTypeHorizontalBar;
if (this.configurationService) {
let chartSettings = WorkbenchUtils.getSqlConfigSection(this.configurationService, 'chart');
// Only use the value if it's a known chart type. Ideally could query this dynamically but can't figure out how
if (chartSettings && Constants.allChartTypes.indexOf(chartSettings[Constants.defaultChartType]) > -1) {
defaultChartType = chartSettings[Constants.defaultChartType];
}
}
return defaultChartType;
}
private _initActionBar() {
this._createInsightAction = this.instantiationService.createInstance(CreateInsightAction);
this._copyAction = this.instantiationService.createInstance(CopyAction);
this._saveAction = this.instantiationService.createInstance(SaveImageAction);
let taskbar = <HTMLElement>this.taskbarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar, this.contextMenuService);
this._actionBar.context = this;
this._actionBar.setContent([
{ action: this._createInsightAction },
{ action: this._copyAction },
{ action: this._saveAction }
]);
}
public onChartChanged(e: ISelectData): void {
this.setDefaultChartConfig();
if ([Constants.chartTypeScatter, Constants.chartTypeTimeSeries].some(item => item === e.selected)) {
this.dataType = DataType.Point;
this.dataDirection = DataDirection.Horizontal;
}
this.initChart();
}
ngAfterViewInit() {
this.initChart();
}
setConfigValue(key: string, value: any, refresh = true): void {
this._chartConfig[key] = value;
if (refresh) {
this.initChart();
}
}
public set dataType(type: DataType) {
this._chartConfig.dataType = type;
// Requires full chart refresh
this.initChart();
}
public set dataDirection(direction: DataDirection) {
this._chartConfig.dataDirection = direction;
// Requires full chart refresh
this.initChart();
}
public copyChart(): void {
let data = this._chartComponent.getCanvasData();
if (!data) {
this.showError(LocalizedStrings.CHART_NOT_FOUND);
return;
}
this.clipboardService.writeImageDataUrl(data);
}
public saveChart(): void {
this.promptForFilepath().then(filePath => {
let data = this._chartComponent.getCanvasData();
if (!data) {
this.showError(LocalizedStrings.CHART_NOT_FOUND);
return;
}
if (filePath) {
let buffer = this.decodeBase64Image(data);
pfs.writeFile(filePath, buffer).then(undefined, (err) => {
if (err) {
this.showError(err.message);
} else {
let fileUri = URI.from({ scheme: PathUtilities.FILE_SCHEMA, path: filePath });
this.windowsService.openExternal(fileUri.toString());
this.notificationService.notify({
severity: Severity.Error,
message: nls.localize('chartSaved', 'Saved Chart to path: {0}', filePath)
});
}
});
}
});
}
private promptForFilepath(): Thenable<string> {
let filepathPlaceHolder = PathUtilities.resolveCurrentDirectory(this.getActiveUriString(), PathUtilities.getRootPath(this.workspaceContextService));
filepathPlaceHolder = paths.join(filepathPlaceHolder, 'chart.png');
return this.windowService.showSaveDialog({
title: nls.localize('chartViewer.saveAsFileTitle', 'Choose Results File'),
defaultPath: paths.normalize(filepathPlaceHolder, true)
});
}
private decodeBase64Image(data: string): Buffer {
let matches = data.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
return new Buffer(matches[2], 'base64');
}
public createInsight(): void {
let uriString: string = this.getActiveUriString();
if (!uriString) {
this.showError(nls.localize('createInsightNoEditor', 'Cannot create insight as the active editor is not a SQL Editor'));
return;
}
let uri: URI = URI.parse(uriString);
let dataService = this.queryModelService.getDataService(uriString);
if (!dataService) {
this.showError(nls.localize('createInsightNoDataService', 'Cannot create insight, backing data model not found'));
return;
}
let queryFile: string = uri.fsPath;
let query: string = undefined;
let type = {};
type[this.chartTypesSelectBox.value] = this._chartConfig;
// create JSON
let config: IInsightsConfig = {
type,
query,
queryFile
};
let widgetConfig = {
name: nls.localize('myWidgetName', 'My-Widget'),
gridItemConfig: this.getGridItemConfig(),
widget: {
'insights-widget': config
}
};
// open in new window as untitled JSON file
dataService.openLink(JSON.stringify(widgetConfig), 'Insight', 'json');
}
private showError(errorMsg: string) {
this.notificationService.notify({
severity: Severity.Error,
message: errorMsg
});
}
private getGridItemConfig(): NgGridItemConfig {
let config: NgGridItemConfig = {
sizex: 2,
sizey: 1
};
return config;
}
private getActiveUriString(): string {
let editorService = this.editorService;
let editor = editorService.activeControl;
if (editor && editor instanceof QueryEditor) {
let queryEditor: QueryEditor = editor;
return queryEditor.uri;
}
return undefined;
}
protected get showDataDirection(): boolean {
return ['pie', 'horizontalBar', 'bar', 'doughnut'].some(item => item === this.chartTypesSelectBox.value) || (this.chartTypesSelectBox.value === 'line' && this.dataType === 'number');
}
protected get showLabelFirstColumn(): boolean {
return this.dataDirection === 'horizontal' && this.dataType !== 'point';
}
protected get showColumnsAsLabels(): boolean {
return this.dataDirection === 'vertical' && this.dataType !== 'point';
}
public get dataDirection(): DataDirection {
return this._chartConfig.dataDirection;
}
public get dataType(): DataType {
return this._chartConfig.dataType;
}
@Input() set dataSet(dataSet: IGridDataSet) {
// Setup the execute result
this._executeResult = <IInsightData>{};
// Remove first column and its value since this is the row number column
this._executeResult.columns = dataSet.columnDefinitions.slice(1).map(def => def.name);
this._executeResult.rows = dataSet.dataRows.getRange(0, dataSet.dataRows.getLength()).map(v => {
return this._executeResult.columns.reduce((p, c) => {
p.push(v[c]);
return p;
}, []);
});
}
public initChart() {
this._cd.detectChanges();
if (this._executeResult) {
// Reinitialize the chart component
let componentFactory = this._componentFactoryResolver.resolveComponentFactory<IInsightsView>(insightRegistry.getCtorFromId(this.chartTypesSelectBox.value));
this.componentHost.viewContainerRef.clear();
let componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory);
this._chartComponent = <ChartInsight>componentRef.instance;
if (this._chartComponent.setConfig) {
this._chartComponent.setConfig(this._chartConfig);
}
this._chartComponent.data = this._executeResult;
this._chartComponent.options = mixin(this._chartComponent.options, { animation: { duration: 0 } });
if (this._chartComponent.init) {
this._chartComponent.init();
}
}
}
ngOnDestroy() {
this._disposables.forEach(i => i.dispose());
}
}

View File

@@ -1,56 +0,0 @@
input[type="radio"] {
margin-top: -2px;
vertical-align: middle;
}
.chart-viewer {
overflow-y: auto;
}
.chart-viewer .indent {
margin-left: 7px;
}
.chart-viewer .radio-indent {
margin-left: 5px;
}
.chart-viewer .option {
width: 100%;
padding-bottom: 7px;
}
.chart-viewer .dialog-label {
width: 100%;
padding-bottom: 5px;
}
.chart-viewer .input-divider {
width: 100%;
padding-bottom: 20px;
}
.chart-viewer .footer {
display: flex;
}
.chart-viewer .footer .right-footer {
display: flex;
flex: 1 1 auto;
justify-content: flex-end;
}
.chart-viewer .footer-button a.monaco-button.monaco-text-button {
width: 100px;
}
.vs-dark.monaco-shell .chart-viewer .footer-button a.monaco-button.monaco-text-button {
outline-color: #8e8c8c;
}
.chart-viewer .footer-button {
margin-right: 5px;
}
.chart-viewer .right-footer .footer-button:last-of-type {
margin-right: none;
}

View File

@@ -1,108 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import { Action } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
export interface IChartViewActionContext {
copyChart(): void;
saveChart(): void;
createInsight(): void;
}
export class ChartViewActionBase extends Action {
public static BaseClass = 'queryTaskbarIcon';
private _classes: string[];
constructor(
id: string,
label: string,
enabledClass: string,
protected notificationService: INotificationService
) {
super(id, label);
this.enabled = true;
this._setCssClass(enabledClass);
}
protected updateCssClass(enabledClass: string): void {
// set the class, useful on change of label or icon
this._setCssClass(enabledClass);
}
/**
* Sets the CSS classes combining the parent and child classes.
* Public for testing only.
*/
private _setCssClass(enabledClass: string): void {
this._classes = [];
this._classes.push(ChartViewActionBase.BaseClass);
if (enabledClass) {
this._classes.push(enabledClass);
}
this.class = this._classes.join(' ');
}
protected doRun(context: IChartViewActionContext, runAction: Function): TPromise<boolean> {
if (!context) {
// TODO implement support for finding chart view in active window
this.notificationService.notify({
severity: Severity.Error,
message: nls.localize('chartContextRequired', 'Chart View context is required to run this action')
});
return TPromise.as(false);
}
return new TPromise<boolean>((resolve, reject) => {
runAction();
resolve(true);
});
}
}
export class CreateInsightAction extends ChartViewActionBase {
public static ID = 'chartview.createInsight';
public static LABEL = nls.localize('createInsightLabel', "Create Insight");
constructor(@INotificationService notificationService: INotificationService
) {
super(CreateInsightAction.ID, CreateInsightAction.LABEL, 'createInsight', notificationService);
}
public run(context: IChartViewActionContext): TPromise<boolean> {
return this.doRun(context, () => context.createInsight());
}
}
export class CopyAction extends ChartViewActionBase {
public static ID = 'chartview.copy';
public static LABEL = nls.localize('copyChartLabel', "Copy as image");
constructor(@INotificationService notificationService: INotificationService
) {
super(CopyAction.ID, CopyAction.LABEL, 'copyImage', notificationService);
}
public run(context: IChartViewActionContext): TPromise<boolean> {
return this.doRun(context, () => context.copyChart());
}
}
export class SaveImageAction extends ChartViewActionBase {
public static ID = 'chartview.saveImage';
public static LABEL = nls.localize('saveImageLabel', "Save as image");
constructor(@INotificationService notificationService: INotificationService
) {
super(SaveImageAction.ID, SaveImageAction.LABEL, 'saveAsImage', notificationService);
}
public run(context: IChartViewActionContext): TPromise<boolean> {
return this.doRun(context, () => context.saveChart());
}
}

View File

@@ -1,82 +0,0 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="fullsize vertBox" style="position: relative">
<div #resultsPane role="toolbar" tabindex="0" [attr.aria-label]="LocalizedConstants.resultPaneLabel" [attr.aria-expanded]="resultActive" *ngIf="dataSets.length > 0" id="resultspane" class="boxRow resultsMessageHeader resultsViewCollapsible" [class.collapsed]="!resultActive" (click)="togglePane('results')">
<span> {{LocalizedConstants.resultPaneLabel}} </span>
<span class="queryResultsShortCut"> {{resultShortcut}} </span>
</div>
<div #resultsScrollBox id="results" *ngIf="renderedDataSets.length > 0" class="results vertBox scrollable"
(onScroll)="onScroll($event)" [scrollEnabled]="scrollEnabled" [class.hidden]="!resultActive"
(focusin)="onGridFocus()" (focusout)="onGridFocusout()">
<div class="boxRow content horzBox slickgrid" *ngFor="let dataSet of renderedDataSets; let i = index"
[style.max-height]="dataSet.maxHeight" [style.min-height]="dataSet.minHeight">
<slick-grid #slickgrid id="slickgrid_{{i}}"
class="boxCol content vertBox slickgrid"
enableAsyncPostRender="true"
showHeader="true"
[columnDefinitions]="dataSet.columnDefinitions"
[ngClass]="i === activeGrid ? 'active' : ''"
[dataRows]="dataSet.dataRows"
[resized]="dataSet.resized"
[selectionModel]="selectionModel"
[plugins]="plugins[i]"
[rowHeight]="rowHeight"
(onContextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)"
(onActiveCellChanged)="onActiveCellChanged(i)"
(mousedown)="navigateToGrid(i)">
</slick-grid>
<span class="boxCol content vertBox">
<div class="boxRow content maxHeight" *ngFor="let icon of dataIcons">
<div *ngIf="icon.showCondition()" class="gridIconContainer">
<a class="gridIcon" style="cursor: pointer;"
role="button"
(click)="icon.functionality(dataSet.batchId, dataSet.resultId, i)"
[title]="icon.hoverText()" [ngClass]="icon.icon()" tabindex="0">
</a>
</div>
</div>
</span>
</div>
</div>
<div id="messagepane" role="toolbar" tabindex="0" [attr.aria-label]="LocalizedConstants.messagePaneLabel" [attr.aria-expanded]="!(!messageActive && dataSets.length !== 0)" class="boxRow resultsMessageHeader resultsViewCollapsible" [class.collapsed]="!messageActive && dataSets.length !== 0" (click)="togglePane('messages')" style="position: relative">
<div id="messageResizeHandle" [class.hidden]="!_resultsPane || !_messageActive || !resultActive" class="resizableHandle"></div>
<span> {{LocalizedConstants.messagePaneLabel}} </span>
<span class="queryResultsShortCut"> {{messageShortcut}} </span>
</div>
<div #messagesContainer id="messages" class="scrollable messages" [class.hidden]="!messageActive && dataSets.length !== 0"
(contextmenu)="openMessagesContextMenu($event)" (focusin)="onMessagesFocus()" (focusout)="onMessagesFocusout()"
tabindex=0>
<div class="messagesTopSpacing"></div>
<table id="messageTable" class="resultsMessageTable">
<colgroup>
<col span="1" class="wideResultsMessage">
</colgroup>
<tbody>
<ng-template ngFor let-message [ngForOf]="messages">
<tr class='messageRow'>
<td><span *ngIf="message.link">[{{message.time}}]</span></td>
<td class="resultsMessageValue" [class.errorMessage]="message.isError" [class.batchMessage]="!message.link">{{message.message}} <a tabindex="0" #queryLink class="queryLink" *ngIf="message.link" (click)="onSelectionLinkClicked(message.batchId)" (keyup)="onKey($event, message.batchId)">{{message.link.text}}</a>
</td>
</tr>
</ng-template>
<tr id='executionSpinner' *ngIf="!complete">
<td><span *ngIf="messages.length === 0">[{{startString}}]</span></td>
<td>
<img class="icon in-progress" height="18px" />
<span style="vertical-align: bottom">{{LocalizedConstants.executeQueryLabel}}</span>
</td>
</tr>
<tr *ngIf="complete">
<td></td>
<td>{{stringsFormat(LocalizedConstants.elapsedTimeLabel, totalElapsedTimeSpan)}}</td>
</tr>
</tbody>
</table>
</div>
<div id="resizeHandle" [class.hidden]="!resizing" [style.top]="resizeHandleTop"></div>
</div>

View File

@@ -1,732 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!sql/media/icons/common-icons';
import 'vs/css!sql/parts/grid/media/slickColorTheme';
import 'vs/css!sql/parts/grid/media/flexbox';
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 {
ElementRef, QueryList, ChangeDetectorRef, OnInit, OnDestroy, Component, Inject,
ViewChildren, forwardRef, EventEmitter, Input, ViewChild
} from '@angular/core';
import { IGridDataRow, SlickGrid, VirtualizedCollection } from 'angular2-slickgrid';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import * as Services from 'sql/parts/grid/services/sharedServices';
import { IGridIcon, IMessage, IGridDataSet } from 'sql/parts/grid/common/interfaces';
import { GridParentComponent } from 'sql/parts/grid/views/gridParentComponent';
import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
import { IQueryComponentParams } from 'sql/services/bootstrap/bootstrapParams';
import { error } from 'sql/base/common/log';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { clone, mixin } from 'sql/base/common/objects';
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
import { escape } from 'sql/base/common/strings';
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin';
import { format } from 'vs/base/common/strings';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
export const QUERY_SELECTOR: string = 'query-component';
declare type PaneType = 'messages' | 'results';
@Component({
selector: QUERY_SELECTOR,
host: { '(window:keydown)': 'keyEvent($event)', '(window:gridnav)': 'keyEvent($event)' },
templateUrl: decodeURI(require.toUrl('sql/parts/grid/views/query/query.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => QueryComponent) }]
})
export class QueryComponent extends GridParentComponent implements OnInit, OnDestroy {
// CONSTANTS
// tslint:disable-next-line:no-unused-variable
private scrollTimeOutTime: number = 200;
private windowSize: number = 50;
private messagePaneHeight: number = 22;
// tslint:disable-next-line:no-unused-variable
private maxScrollGrids: number = 8;
// create a function alias to use inside query.component
// tslint:disable-next-line:no-unused-variable
protected stringsFormat: any = format;
protected plugins = new Array<Array<Slick.Plugin<any>>>();
// tslint:disable-next-line:no-unused-variable
private dataIcons: IGridIcon[] = [
{
showCondition: () => { return this.dataSets.length > 1; },
icon: () => {
return this.renderedDataSets.length === 1
? 'exitFullScreen'
: 'extendFullScreen';
},
hoverText: () => {
return this.renderedDataSets.length === 1
? LocalizedConstants.restoreLabel
: LocalizedConstants.maximizeLabel;
},
functionality: (batchId, resultId, index) => {
this.magnify(index);
}
},
{
showCondition: () => { return true; },
icon: () => { return 'saveCsv'; },
hoverText: () => { return LocalizedConstants.saveCSVLabel; },
functionality: (batchId, resultId, index) => {
let selection = this.getSelection(index);
if (selection.length <= 1) {
this.handleContextClick({ type: 'savecsv', batchId: batchId, resultId: resultId, index: index, selection: selection });
} else {
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
}
}
},
{
showCondition: () => { return true; },
icon: () => { return 'saveJson'; },
hoverText: () => { return LocalizedConstants.saveJSONLabel; },
functionality: (batchId, resultId, index) => {
let selection = this.getSelection(index);
if (selection.length <= 1) {
this.handleContextClick({ type: 'savejson', batchId: batchId, resultId: resultId, index: index, selection: selection });
} else {
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
}
}
},
{
showCondition: () => { return true; },
icon: () => { return 'saveExcel'; },
hoverText: () => { return LocalizedConstants.saveExcelLabel; },
functionality: (batchId, resultId, index) => {
let selection = this.getSelection(index);
if (selection.length <= 1) {
this.handleContextClick({ type: 'saveexcel', batchId: batchId, resultId: resultId, index: index, selection: selection });
} else {
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
}
}
},
{
showCondition: () => { return true; },
icon: () => { return 'saveXml'; },
hoverText: () => { return LocalizedConstants.saveXMLLabel; },
functionality: (batchId, resultId, index) => {
let selection = this.getSelection(index);
if (selection.length <= 1) {
this.handleContextClick({ type: 'savexml', batchId: batchId, resultId: resultId, index: index, selection: selection });
} else {
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
}
}
},
{
showCondition: () => {
return this.configurationService.getValue('workbench')['enablePreviewFeatures'];
},
icon: () => { return 'viewChart'; },
hoverText: () => { return LocalizedConstants.viewChartLabel; },
functionality: (batchId, resultId, index) => {
this.showChartForGrid(index);
}
}
];
// FIELDS
// Service for interaction with the IQueryModel
// All datasets
private dataSets: IGridDataSet[] = [];
private messages: IMessage[] = [];
private messageStore: IMessage[] = [];
private messageTimeout: number;
private lastMessageHandleTime: number = 0;
private scrollTimeOut: number;
private resizing = false;
private resizeHandleTop: string = '0';
private scrollEnabled = true;
private rowHeight: number;
// tslint:disable-next-line:no-unused-variable
private firstRender = true;
private totalElapsedTimeSpan: number;
private complete = false;
private sentPlans: Map<number, string> = new Map<number, string>();
private hasQueryPlan: boolean = false;
private queryPlanResultSetId: number = 0;
public queryExecutionStatus: EventEmitter<string> = new EventEmitter<string>();
public queryPlanAvailable: EventEmitter<string> = new EventEmitter<string>();
public showChartRequested: EventEmitter<IGridDataSet> = new EventEmitter<IGridDataSet>();
public goToNextQueryOutputTabRequested: EventEmitter<void> = new EventEmitter<void>();
public onActiveCellChanged: (gridIndex: number) => void;
private savedViewState: {
gridSelections: Slick.Range[][];
resultsScroll: number;
messagePaneScroll: number;
slickGridScrolls: { vertical: number; horizontal: number }[];
};
@Input() public queryParameters: IQueryComponentParams;
@ViewChildren('slickgrid') slickgrids: QueryList<SlickGrid>;
// tslint:disable-next-line:no-unused-variable
@ViewChild('resultsPane', { read: ElementRef }) private _resultsPane: ElementRef;
@ViewChild('queryLink', { read: ElementRef }) private _queryLinkElement: ElementRef;
@ViewChild('messagesContainer', { read: ElementRef }) private _messagesContainer: ElementRef;
@ViewChild('resultsScrollBox', { read: ElementRef }) private _resultsScrollBox: ElementRef;
@ViewChildren('slickgrid', { read: ElementRef }) private _slickgridElements: QueryList<ElementRef>;
constructor(
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IContextKeyService) contextKeyService: IContextKeyService,
@Inject(IConfigurationService) configurationService: IConfigurationService,
@Inject(IClipboardService) clipboardService: IClipboardService,
@Inject(IQueryEditorService) queryEditorService: IQueryEditorService,
@Inject(INotificationService) notificationService: INotificationService,
) {
super(el, cd, contextMenuService, keybindingService, contextKeyService, configurationService, clipboardService, queryEditorService, notificationService);
this._el.nativeElement.className = 'slickgridContainer';
this.rowHeight = configurationService.getValue<any>('resultsGrid').rowHeight;
configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('resultsGrid')) {
this.rowHeight = configurationService.getValue<any>('resultsGrid').rowHeight;
this.slickgrids.forEach(i => {
i.rowHeight = this.rowHeight;
});
this.resizeGrids();
}
});
}
/**
* Called by Angular when the object is initialized
*/
ngOnInit(): void {
const self = this;
this.dataService = this.queryParameters.dataService;
this.actionProvider = this.instantiationService.createInstance(GridActionProvider, this.dataService, this.onGridSelectAll());
this.baseInit();
this.setupResizeBind();
this.subscribeWithDispose(this.dataService.queryEventObserver, (event) => {
switch (event.type) {
case 'start':
self.handleStart(self, event);
break;
case 'complete':
self.handleComplete(self, event);
break;
case 'message':
self.handleMessage(self, event);
break;
case 'resultSet':
self.handleResultSet(self, event);
break;
default:
error('Unexpected query event type "' + event.type + '" sent');
break;
}
self._cd.detectChanges();
});
this.queryParameters.onSaveViewState(() => this.saveViewState());
this.queryParameters.onRestoreViewState(() => this.restoreViewState());
this.dataService.onAngularLoaded();
}
public ngOnDestroy(): void {
this.baseDestroy();
}
protected initShortcuts(shortcuts: { [name: string]: Function }): void {
shortcuts['event.nextGrid'] = () => {
this.navigateToGrid(this.activeGrid + 1);
};
shortcuts['event.prevGrid'] = () => {
this.navigateToGrid(this.activeGrid - 1);
};
shortcuts['event.maximizeGrid'] = () => {
this.magnify(this.activeGrid);
};
}
handleStart(self: QueryComponent, event: any): void {
self.messages = [];
self.dataSets = [];
self.placeHolderDataSets = [];
self.renderedDataSets = self.placeHolderDataSets;
self.totalElapsedTimeSpan = undefined;
self.complete = false;
self.activeGrid = 0;
this.onActiveCellChanged = this.onCellSelect;
// reset query plan info and send notification to subscribers
self.hasQueryPlan = false;
self.sentPlans = new Map<number, string>();
self.queryExecutionStatus.emit('start');
self.firstRender = true;
}
handleComplete(self: QueryComponent, event: any): void {
self.totalElapsedTimeSpan = event.data;
self.complete = true;
}
handleMessage(self: QueryComponent, event: any): void {
self.messageStore.push(event.data);
// Ensure that messages are updated at least every 10 seconds during long-running queries
if (self.messageTimeout !== undefined && Date.now() - self.lastMessageHandleTime < 10000) {
clearTimeout(self.messageTimeout);
} else {
self.lastMessageHandleTime = Date.now();
}
self.messageTimeout = setTimeout(() => {
while (self.messageStore.length > 0) {
let lastMessage = self.messages.length > 0 ? self.messages[self.messages.length - 1] : undefined;
let nextMessage = self.messageStore[0];
// If the next message has the same metadata as the previous one, just append its text to avoid rendering an entirely new message
if (lastMessage !== undefined && lastMessage.batchId === nextMessage.batchId && lastMessage.isError === nextMessage.isError
&& lastMessage.link === nextMessage.link && lastMessage.link === undefined) {
lastMessage.message += '\n' + nextMessage.message;
} else {
self.messages.push(nextMessage);
}
self.messageStore = self.messageStore.slice(1);
}
self._cd.detectChanges();
self.scrollMessages();
}, 100);
}
handleResultSet(self: QueryComponent, event: any): void {
let resultSet = event.data;
// No column info found, so define a column of no name by default
if (!resultSet.columnInfo) {
resultSet.columnInfo = [];
resultSet.columnInfo[0] = { columnName: '' };
}
// Setup a function for generating a promise to lookup result subsets
let loadDataFunction = (offset: number, count: number): Promise<IGridDataRow[]> => {
return new Promise<IGridDataRow[]>((resolve, reject) => {
self.dataService.getQueryRows(offset, count, resultSet.batchId, resultSet.id).subscribe(rows => {
let gridData: IGridDataRow[] = [];
for (let row = 0; row < rows.rows.length; row++) {
// Push row values onto end of gridData for slickgrid
gridData.push({
values: [{}].concat(rows.rows[row].map(c => {
return mixin({ ariaLabel: escape(c.displayValue) }, c);
}))
});
}
// if this is a query plan resultset we haven't processed yet then forward to subscribers
if (self.hasQueryPlan && resultSet.id === self.queryPlanResultSetId && !self.sentPlans[resultSet.id]) {
self.sentPlans[resultSet.id] = rows.rows[0][0].displayValue;
self.queryPlanAvailable.emit(rows.rows[0][0].displayValue);
}
resolve(gridData);
});
});
};
// Precalculate the max height and min height
let maxHeight: string = 'inherit';
if (resultSet.rowCount < self._defaultNumShowingRows) {
let maxHeightNumber: number = Math.max((resultSet.rowCount + 1) * self._rowHeight, self.dataIcons.length * 30) + 10;
maxHeight = maxHeightNumber.toString() + 'px';
}
let minHeight: string = maxHeight;
if (resultSet.rowCount >= self._defaultNumShowingRows) {
let minHeightNumber: number = (self._defaultNumShowingRows + 1) * self._rowHeight + 10;
minHeight = minHeightNumber.toString() + 'px';
}
let rowNumberColumn = new RowNumberColumn({ numberOfRows: resultSet.rowCount });
// Store the result set from the event
let dataSet: IGridDataSet = {
resized: undefined,
batchId: resultSet.batchId,
resultId: resultSet.id,
totalRows: resultSet.rowCount,
maxHeight: maxHeight,
minHeight: minHeight,
dataRows: new VirtualizedCollection(
self.windowSize,
resultSet.rowCount,
loadDataFunction,
index => { return { values: [] }; }
),
columnDefinitions: [rowNumberColumn.getColumnDefinition()].concat(resultSet.columnInfo.map((c, i) => {
let isLinked = c.isXml || c.isJson;
let linkType = c.isXml ? 'xml' : 'json';
return {
id: i.toString(),
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
? 'XML Showplan'
: escape(c.columnName),
field: i.toString(),
formatter: isLinked ? Services.hyperLinkFormatter : Services.textFormatter,
asyncPostRender: isLinked ? self.linkHandler(linkType) : undefined
};
}))
};
self.plugins.push([rowNumberColumn, new AutoColumnSize(), new AdditionalKeyBindings()]);
self.dataSets.push(dataSet);
// check if the resultset is for a query plan
for (let i = 0; i < resultSet.columnInfo.length; ++i) {
let column = resultSet.columnInfo[i];
if (column.columnName === 'Microsoft SQL Server 2005 XML Showplan') {
this.hasQueryPlan = true;
this.queryPlanResultSetId = resultSet.id;
break;
}
}
// Create a dataSet to render without rows to reduce DOM size
let undefinedDataSet = clone(dataSet);
undefinedDataSet.columnDefinitions = dataSet.columnDefinitions;
undefinedDataSet.dataRows = undefined;
undefinedDataSet.resized = new EventEmitter();
self.placeHolderDataSets.push(undefinedDataSet);
self.onScroll(0);
}
onCellSelect(gridIndex: number): void {
this.activeGrid = gridIndex;
}
openMessagesContextMenu(event: any): void {
let self = this;
event.preventDefault();
let selectedRange = this.getSelectedRangeUnderMessages();
let selectAllFunc = () => self.selectAllMessages();
let anchor = { x: event.x + 1, y: event.y };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this.actionProvider.getMessagesActions(this.dataService, selectAllFunc),
getKeyBinding: (action) => this._keybindingFor(action),
onHide: (wasCancelled?: boolean) => {
},
getActionsContext: () => (selectedRange)
});
}
/**
* Handles rendering the results to the DOM that are currently being shown
* and destroying any results that have moved out of view
* @param scrollTop The scrolltop value, if not called by the scroll event should be 0
*/
onScroll(scrollTop): void {
const self = this;
clearTimeout(self.scrollTimeOut);
this.scrollTimeOut = setTimeout(() => {
if (self.dataSets.length < self.maxScrollGrids) {
self.scrollEnabled = false;
for (let i = 0; i < self.placeHolderDataSets.length; i++) {
self.placeHolderDataSets[i].dataRows = self.dataSets[i].dataRows;
self.placeHolderDataSets[i].resized.emit();
}
} else {
let gridHeight = self._el.nativeElement.getElementsByTagName('slick-grid')[0].offsetHeight;
let tabHeight = self.getResultsElement().offsetHeight;
let numOfVisibleGrids = Math.ceil((tabHeight / gridHeight)
+ ((scrollTop % gridHeight) / gridHeight));
let min = Math.floor(scrollTop / gridHeight);
let max = min + numOfVisibleGrids;
for (let i = 0; i < self.placeHolderDataSets.length; i++) {
if (i >= min && i < max) {
if (self.placeHolderDataSets[i].dataRows === undefined) {
self.placeHolderDataSets[i].dataRows = self.dataSets[i].dataRows;
self.placeHolderDataSets[i].resized.emit();
}
} else if (self.placeHolderDataSets[i].dataRows !== undefined) {
self.placeHolderDataSets[i].dataRows = undefined;
}
}
}
self._cd.detectChanges();
}, self.scrollTimeOutTime);
}
onSelectionLinkClicked(index: number): void {
this.dataService.setEditorSelection(index);
}
onKey(e: Event, index: number) {
if (DOM.isAncestor(<HTMLElement>e.target, this._queryLinkElement.nativeElement) && e instanceof KeyboardEvent) {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter)) {
this.onSelectionLinkClicked(index);
e.stopPropagation();
}
}
}
/**
* Sets up the resize for the messages/results panes bar
*/
setupResizeBind(): void {
const self = this;
let resizeHandleElement: HTMLElement = self._el.nativeElement.querySelector('#messageResizeHandle');
let $resizeHandle = $(resizeHandleElement);
let $messages = $(self.getMessagesElement());
$resizeHandle.bind('dragstart', (e) => {
self.resizing = true;
self.resizeHandleTop = self.calculateResizeHandleTop(e.pageY);
self._cd.detectChanges();
return true;
});
$resizeHandle.bind('drag', (e) => {
// Update the animation if the drag is within the allowed range.
if (self.isDragWithinAllowedRange(e.pageY, resizeHandleElement)) {
self.resizeHandleTop = self.calculateResizeHandleTop(e.pageY);
self.resizing = true;
self._cd.detectChanges();
// Stop the animation if the drag is out of the allowed range.
// The animation is resumed when the drag comes back into the allowed range.
} else {
self.resizing = false;
}
});
$resizeHandle.bind('dragend', (e) => {
self.resizing = false;
// Redefine the min size for the messages based on the final position
// if the drag is within the allowed rang
if (self.isDragWithinAllowedRange(e.pageY, resizeHandleElement)) {
let minHeightNumber = this.getMessagePaneHeightFromDrag(e.pageY);
$messages.css('min-height', minHeightNumber + 'px');
self._cd.detectChanges();
self.resizeGrids();
// Otherwise just update the UI to show that the drag is complete
} else {
self._cd.detectChanges();
}
});
}
/**
* Returns true if the resize of the messagepane given by the drag at top=eventPageY is valid,
* false otherwise. A drag is valid if it is below the bottom of the resultspane and
* this.messagePaneHeight pixels above the bottom of the entire angular component.
*/
isDragWithinAllowedRange(eventPageY: number, resizeHandle: HTMLElement): boolean {
let resultspaneElement: HTMLElement = this._el.nativeElement.querySelector('#resultspane');
let minHeight = this.getMessagePaneHeightFromDrag(eventPageY);
if (resultspaneElement &&
minHeight > 0 &&
resultspaneElement.getBoundingClientRect().bottom < eventPageY
) {
return true;
}
return false;
}
/**
* Calculates the position of the top of the resize handle given the Y-axis drag
* coordinate as eventPageY.
*/
calculateResizeHandleTop(eventPageY: number): string {
let resultsWindowTop: number = this._el.nativeElement.getBoundingClientRect().top;
let relativeTop: number = eventPageY - resultsWindowTop;
return relativeTop + 'px';
}
/**
* Returns the height the message pane would be if it were resized so that its top would be set to eventPageY.
* This will return a negative value if eventPageY is below the bottom limit.
*/
getMessagePaneHeightFromDrag(eventPageY: number): number {
let bottomDragLimit: number = this._el.nativeElement.getBoundingClientRect().bottom - this.messagePaneHeight;
return bottomDragLimit - eventPageY;
}
/**
* Ensures the messages tab is scrolled to the bottom
*/
scrollMessages(): void {
let messagesDiv = this.getMessagesElement();
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
/**
*
*/
protected tryHandleKeyEvent(e): boolean {
return false;
}
/**
* Handles rendering and unrendering necessary resources in order to properly
* navigate from one grid another. Should be called any time grid navigation is performed
* @param targetIndex The index in the renderedDataSets to navigate to
* @returns A boolean representing if the navigation was successful
*/
navigateToGrid(targetIndex: number): boolean {
// check if the target index is valid
if (targetIndex >= this.renderedDataSets.length || !this.hasFocus()) {
return false;
}
// Deselect any text since we are navigating to a new grid
// Do this even if not switching grids, since this covers clicking on the grid after message selection
window.getSelection().removeAllRanges();
// check if you are actually trying to change navigation
if (this.activeGrid === targetIndex) {
return false;
}
this.slickgrids.toArray()[this.activeGrid].selection = false;
this.slickgrids.toArray()[targetIndex].setActive();
this.activeGrid = targetIndex;
// scrolling logic
let resultsWindow = $('#results');
let scrollTop = resultsWindow.scrollTop();
let scrollBottom = scrollTop + resultsWindow.height();
let gridHeight = $(this._el.nativeElement).find('slick-grid').height();
if (scrollBottom < gridHeight * (targetIndex + 1)) {
scrollTop += (gridHeight * (targetIndex + 1)) - scrollBottom;
resultsWindow.scrollTop(scrollTop);
}
if (scrollTop > gridHeight * targetIndex) {
scrollTop = (gridHeight * targetIndex);
resultsWindow.scrollTop(scrollTop);
}
return true;
}
public hasFocus(): boolean {
return DOM.isAncestor(document.activeElement, this._el.nativeElement);
}
resizeGrids(): void {
const self = this;
setTimeout(() => {
for (let grid of self.renderedDataSets) {
grid.resized.emit();
}
});
}
protected showChartForGrid(index: number) {
if (this.renderedDataSets.length > index) {
this.showChartRequested.emit(this.renderedDataSets[index]);
}
}
protected goToNextQueryOutputTab(): void {
this.goToNextQueryOutputTabRequested.emit();
}
protected toggleResultPane(): void {
this.resultActive = !this.resultActive;
this._cd.detectChanges();
if (this.resultActive) {
this.resizeGrids();
this.slickgrids.toArray()[this.activeGrid].setActive();
}
}
protected toggleMessagePane(): void {
this.messageActive = !this.messageActive;
this._cd.detectChanges();
if (this.messageActive && this._messagesContainer) {
let header = <HTMLElement>this._messagesContainer.nativeElement;
header.focus();
}
}
/* Helper function to toggle messages and results panes */
// tslint:disable-next-line:no-unused-variable
private togglePane(pane: PaneType): void {
if (pane === 'messages') {
this.toggleMessagePane();
} else if (pane === 'results') {
this.toggleResultPane();
}
}
private saveViewState(): void {
let gridSelections = this.slickgrids.map(grid => grid.getSelectedRanges());
let resultsScrollElement = (this._resultsScrollBox.nativeElement as HTMLElement);
let resultsScroll = resultsScrollElement.scrollTop;
let messagePaneScroll = (this._messagesContainer.nativeElement as HTMLElement).scrollTop;
let slickGridScrolls = this._slickgridElements.map(element => {
// Get the slick grid's viewport element and save its scroll position
let scrollElement = (element.nativeElement as HTMLElement).children[0].children[3];
return {
vertical: scrollElement.scrollTop,
horizontal: scrollElement.scrollLeft
};
});
this.savedViewState = {
gridSelections,
messagePaneScroll,
resultsScroll,
slickGridScrolls
};
}
private restoreViewState(): void {
if (this.savedViewState) {
this.slickgrids.forEach((grid, index) => grid.selection = this.savedViewState.gridSelections[index]);
(this._resultsScrollBox.nativeElement as HTMLElement).scrollTop = this.savedViewState.resultsScroll;
(this._messagesContainer.nativeElement as HTMLElement).scrollTop = this.savedViewState.messagePaneScroll;
this._slickgridElements.forEach((element, index) => {
let scrollElement = (element.nativeElement as HTMLElement).children[0].children[3];
let savedScroll = this.savedViewState.slickGridScrolls[index];
scrollElement.scrollTop = savedScroll.vertical;
scrollElement.scrollLeft = savedScroll.horizontal;
});
this.savedViewState = undefined;
}
}
layout() {
this.resizeGrids();
}
}