SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View File

@@ -0,0 +1,34 @@
<!--
/*---------------------------------------------------------------------------------------------
* 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 editdata-component">
<div id="results" *ngIf="renderedDataSets.length > 0" class="results vertBox scrollable"
(onScroll)="onScroll($event)" [class.hidden]="!resultActive">
<div class="boxRow content horzBox slickgrid editable" *ngFor="let dataSet of renderedDataSets; let i = index"
[style.max-height]="dataSet.maxHeight" [style.min-height]="dataSet.minHeight">
<slick-grid #slickgrid id="slickgrid_{{i}}" [columnDefinitions]="dataSet.columnDefinitions"
[ngClass]="i === activeGrid ? 'active' : ''"
[dataRows]="dataSet.dataRows"
enableAsyncPostRender="true"
showDataTypeIcon="false"
showHeader="true"
[resized]="dataSet.resized"
[plugins]="slickgridPlugins"
(cellEditBegin)="onCellEditBegin($event)"
(cellEditExit)="onCellEditEnd($event)"
(rowEditBegin)="onRowEditBegin($event)"
(rowEditExit)="onRowEditEnd($event)"
(contextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)"
[isColumnEditable]="onIsColumnEditable"
[isCellEditValid]="onIsCellEditValid"
[overrideCellFn]="overrideCellFn"
enableEditing="true"
class="boxCol content vertBox slickgrid">
</slick-grid>
</div>
</div>
</div>

View File

@@ -0,0 +1,496 @@
/*---------------------------------------------------------------------------------------------
* 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/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 'vs/css!./media/editData';
import { ElementRef, ChangeDetectorRef, OnInit, OnDestroy, Component, Inject, forwardRef, EventEmitter } from '@angular/core';
import { IGridDataRow, VirtualizedCollection } from 'angular2-slickgrid';
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
import * as Services from 'sql/parts/grid/services/sharedServices';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import { EditDataComponentParams } from 'sql/services/bootstrap/bootstrapParams';
import { GridParentComponent } from 'sql/parts/grid/views/gridParentComponent';
import { EditDataGridActionProvider } from 'sql/parts/grid/views/editData/editDataGridActions';
import { error } from 'sql/base/common/log';
import { clone } from 'vs/base/common/objects';
export const EDITDATA_SELECTOR: string = 'editdata-component';
@Component({
selector: EDITDATA_SELECTOR,
host: { '(window:keydown)': 'keyEvent($event)', '(window:gridnav)': 'keyEvent($event)' },
templateUrl: decodeURI(require.toUrl('sql/parts/grid/views/editData/editData.component.html'))
})
export class EditDataComponent extends GridParentComponent implements OnInit, OnDestroy {
// CONSTANTS
private scrollTimeOutTime = 200;
private windowSize = 50;
// FIELDS
// All datasets
private dataSet: IGridDataSet;
private scrollTimeOut: number;
private messagesAdded = false;
private scrollEnabled = true;
private firstRender = true;
private totalElapsedTimeSpan: number;
private complete = false;
private idMapping: { [row: number]: number } = {};
private currentCell: { row: number, column: number } = null;
private rowEditInProgress: boolean = false;
private removingNewRow: boolean = false;
// Edit Data functions
public onCellEditEnd: (event: { row: number, column: number, newValue: any }) => void;
public onCellEditBegin: (event: { row: number, column: number }) => void;
public onRowEditBegin: (event: { row: number }) => void;
public onRowEditEnd: (event: { row: number }) => void;
public onIsCellEditValid: (row: number, column: number, newValue: any) => boolean;
public onIsColumnEditable: (column: number) => boolean;
public overrideCellFn: (rowNumber, columnId, value?, data?) => string;
public loadDataFunction: (offset: number, count: number) => Promise<IGridDataRow[]>;
constructor(
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
@Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService
) {
super(el, cd, bootstrapService);
this._el.nativeElement.className = 'slickgridContainer';
let editDataParameters: EditDataComponentParams = this._bootstrapService.getBootstrapParams(this._el.nativeElement.tagName);
this.dataService = editDataParameters.dataService;
this.actionProvider = new EditDataGridActionProvider(this.dataService, this.onGridSelectAll(), this.onDeleteRow(), this.onRevertRow());
}
/**
* Called by Angular when the object is initialized
*/
ngOnInit(): void {
const self = this;
this.baseInit();
// Add the subscription to the list of things to be disposed on destroy, or else on a new component init
// may get the "destroyed" object still getting called back.
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;
case 'editSessionReady':
self.handleEditSessionReady(self, event);
break;
default:
error('Unexpected query event type "' + event.type + '" sent');
break;
}
self._cd.detectChanges();
});
this.dataService.onAngularLoaded();
}
protected initShortcuts(shortcuts: { [name: string]: Function }): void {
// TODO add any Edit Data-specific shortcuts here
}
public ngOnDestroy(): void {
this.baseDestroy();
}
handleStart(self: EditDataComponent, event: any): void {
self.dataSet = undefined;
self.placeHolderDataSets = [];
self.renderedDataSets = self.placeHolderDataSets;
self.totalElapsedTimeSpan = undefined;
self.complete = false;
self.messagesAdded = false;
// Hooking up edit functions
this.onIsCellEditValid = (row, column, value): boolean => {
// TODO can only run sync code
return true;
};
this.onCellEditEnd = (event: { row: number, column: number, newValue: any }): void => {
self.rowEditInProgress = true;
// Update the cell accordingly
self.dataService.updateCell(this.idMapping[event.row], event.column, event.newValue)
.then(
result => {
self.setCellDirtyState(event.row, event.column + 1, result.cell.isDirty);
self.setRowDirtyState(event.row, result.isRowDirty);
},
error => {
// On error updating cell, jump back to the cell that was being edited
self.focusCell(event.row, event.column + 1);
}
);
};
this.onCellEditBegin = (event: { row: number, column: number }): void => { };
this.onRowEditBegin = (event: { row: number }): void => { };
this.onRowEditEnd = (event: { row: number }): void => { };
this.onIsColumnEditable = (column: number): boolean => {
let result = false;
// Check that our variables exist
if (column !== undefined && !!this.dataSet && !!this.dataSet.columnDefinitions[column]) {
result = this.dataSet.columnDefinitions[column].isEditable;
}
// If no column definition exists then the row is not editable
return result;
};
this.overrideCellFn = (rowNumber, columnId, value?, data?): string => {
let returnVal = '';
if (Services.DBCellValue.isDBCellValue(value)) {
returnVal = value.displayValue;
} else if (typeof value === 'string') {
returnVal = value;
}
return returnVal;
};
// Setup a function for generating a promise to lookup result subsets
this.loadDataFunction = (offset: number, count: number): Promise<IGridDataRow[]> => {
return new Promise<IGridDataRow[]>((resolve, reject) => {
self.dataService.getEditRows(offset, count).subscribe(result => {
let rowIndex = offset;
let gridData: IGridDataRow[] = result.subset.map(row => {
self.idMapping[rowIndex] = row.id;
rowIndex++;
return { values: row.cells, row: row.id };
});
// Append a NULL row to the end of gridData
let newLastRow = gridData.length === 0 ? 0 : (gridData[gridData.length - 1].row + 1);
gridData.push({ values: self.dataSet.columnDefinitions.map(cell => { return { displayValue: 'NULL', isNull: false }; }), row: newLastRow });
resolve(gridData);
});
});
};
}
onDeleteRow(): (index: number) => void {
const self = this;
return (index: number): void => {
self.dataService.deleteRow(index).then(() => {
self.dataService.commitEdit().then(() => {
self.removeRow(index, 0);
});
});
};
}
onRevertRow(): (index: number) => void {
const self = this;
return (index: number): void => {
// Force focus to the first cell (completing any active edit operation)
self.focusCell(index, 0, false);
// Perform a revert row operation
self.dataService.revertRow(index)
.then(() => { self.dataService.commitEdit(); })
.then(() => { self.refreshResultsets(); });
};
}
onCellSelect(row: number, column: number): void {
let self = this;
// TODO: We can skip this step if we're allowing multiple commits
if (this.rowEditInProgress) {
// We're in the middle of a row edit, so we need to commit if we move to a different row
if (row !== this.currentCell.row) {
this.dataService.commitEdit()
.then(
result => {
// Committing was successful. Clean the grid, turn off the row edit flag, then select again
self.setGridClean();
self.rowEditInProgress = false;
self.onCellSelect(row, column);
}, error => {
// Committing failed, so jump back to the last selected cell
self.focusCell(self.currentCell.row, self.currentCell.column);
}
);
return;
}
} else {
// We're not in the middle of a row edit, so we can move anywhere
// Checking for removing new row makes sure we don't re-add the new row after we've
// jumped to the first cell of the "new row"
if (this.isNullRow(row) && !this.removingNewRow) {
// We moved into the "new row", add another new row
this.addRow(row, column);
}
}
// Set the cell we moved to as the current cell
this.currentCell = { row: row, column: column };
}
handleComplete(self: EditDataComponent, event: any): void {
self.totalElapsedTimeSpan = event.data;
self.complete = true;
self.messagesAdded = true;
}
handleEditSessionReady(self, event): void {
// TODO: update when edit session is ready
}
handleMessage(self: EditDataComponent, event: any): void {
// TODO: what do we do with messages?
}
handleResultSet(self: EditDataComponent, event: any): void {
// Clone the data before altering it to avoid impacting other subscribers
let resultSet = Object.assign({}, event.data);
// Add an extra 'new row'
resultSet.rowCount++;
// Precalculate the max height and min height
let maxHeight = this.getMaxHeight(resultSet.rowCount);
let minHeight = this.getMinHeight(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,
this.loadDataFunction,
index => { return { values: [] }; }
),
columnDefinitions: 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'
: c.columnName,
type: self.stringToFieldType('string'),
formatter: isLinked ? Services.hyperLinkFormatter : Services.textFormatter,
asyncPostRender: isLinked ? self.linkHandler(linkType) : undefined,
isEditable: c.isUpdatable
};
})
};
self.dataSet = dataSet;
// 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.messagesAdded = true;
self.onScroll(0);
// Reset selected cell state
this.currentCell = null;
this.rowEditInProgress = false;
this.removingNewRow = false;
// HACK: unsafe reference to the slickgrid object
// TODO: Reimplement by adding selectCell event to angular2-slickgrid
self._cd.detectChanges();
let slick: any = self.slickgrids.toArray()[0];
let grid: Slick.Grid<any> = slick._grid;
grid.onActiveCellChanged.subscribe((event: Slick.EventData, data: Slick.OnActiveCellChangedEventArgs<any>) => {
self.onCellSelect(data.row, data.cell);
});
}
/**
* 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(() => {
self.scrollEnabled = false;
for (let i = 0; i < self.placeHolderDataSets.length; i++) {
self.placeHolderDataSets[i].dataRows = self.dataSet.dataRows;
self.placeHolderDataSets[i].resized.emit();
}
self._cd.detectChanges();
if (self.firstRender) {
let setActive = function () {
if (self.firstRender && self.slickgrids.toArray().length > 0) {
self.slickgrids.toArray()[0].setActive();
self.firstRender = false;
}
};
setTimeout(() => {
setActive();
});
}
}, self.scrollTimeOutTime);
}
protected tryHandleKeyEvent(e): boolean {
let handled: boolean = false;
// If the esc key was pressed while in a create session
let currentNewRowIndex = this.dataSet.totalRows - 2;
if (e.keyCode === jQuery.ui.keyCode.ESCAPE && this.currentCell.row === currentNewRowIndex) {
// revert our last new row
this.removingNewRow = true;
this.dataService.revertRow(this.idMapping[currentNewRowIndex])
.then(() => {
this.removeRow(currentNewRowIndex, 0);
this.rowEditInProgress = false;
});
handled = true;
}
return handled;
}
// Private Helper Functions ////////////////////////////////////////////////////////////////////////////
// Checks if input row is our NULL new row
private isNullRow(row: number): boolean {
// Null row is always at index (totalRows - 1)
return (row === this.dataSet.totalRows - 1);
}
// Adds CSS classes to slickgrid cells to indicate a dirty state
private setCellDirtyState(row: number, column: number, dirtyState: boolean): void {
let slick: any = this.slickgrids.toArray()[0];
let grid = slick._grid;
if (dirtyState) {
// Change cell color
$(grid.getCellNode(row, column)).addClass('dirtyCell').removeClass('selected');
} else {
$(grid.getCellNode(row, column)).removeClass('dirtyCell');
}
}
// Adds CSS classes to slickgrid rows to indicate a dirty state
private setRowDirtyState(row: number, dirtyState: boolean): void {
let slick: any = this.slickgrids.toArray()[0];
let grid = slick._grid;
if (dirtyState) {
// Change row header color
$(grid.getCellNode(row, 0)).addClass('dirtyRowHeader');
} else {
$(grid.getCellNode(row, 0)).removeClass('dirtyRowHeader');
}
}
// Sets CSS to clean the entire grid of dirty state cells and rows
private setGridClean(): void {
// Remove dirty classes from the entire table
let allRows = $($('.grid-canvas').children());
let allCells = $(allRows.children());
allCells.removeClass('dirtyCell').removeClass('dirtyRowHeader');
}
// Adds an extra row to the end of slickgrid (just for rendering purposes)
// Then sets the focused call afterwards
private addRow(row: number, column: number): void {
// Add a new row to the edit session in the tools service
this.dataService.createRow();
// Adding an extra row for 'new row' functionality
this.rowEditInProgress = true;
this.dataSet.totalRows++;
this.dataSet.maxHeight = this.getMaxHeight(this.dataSet.totalRows);
this.dataSet.minHeight = this.getMinHeight(this.dataSet.totalRows);
this.dataSet.dataRows = new VirtualizedCollection(
this.windowSize,
this.dataSet.totalRows,
this.loadDataFunction,
index => { return { values: [] }; }
);
// Refresh grid
this.onScroll(0);
// Mark the row as dirty once the scroll has completed
setTimeout(() => {
this.setRowDirtyState(row, true);
}, this.scrollTimeOutTime);
}
// removes a row from the end of slickgrid (just for rendering purposes)
// Then sets the focused call afterwards
private removeRow(row: number, column: number): void {
// Removing the new row
this.dataSet.totalRows--;
this.dataSet.dataRows = new VirtualizedCollection(
this.windowSize,
this.dataSet.totalRows,
this.loadDataFunction,
index => { return { values: [] }; }
);
// refresh results view
this.onScroll(0);
// Set focus to the row index column of the removed row
setTimeout(() => {
this.focusCell(row, 0);
this.removingNewRow = false;
}, this.scrollTimeOutTime);
}
private focusCell(row: number, column: number, forceEdit: boolean=true): void {
let slick: any = this.slickgrids.toArray()[0];
let grid = slick._grid;
grid.gotoCell(row, column, forceEdit);
}
private getMaxHeight(rowCount: number): any {
return rowCount < this._defaultNumShowingRows
? ((rowCount + 1) * this._rowHeight) + 10
: 'inherit';
}
private getMinHeight(rowCount: number): any {
return rowCount > this._defaultNumShowingRows
? (this._defaultNumShowingRows + 1) * this._rowHeight + 10
: this.getMaxHeight(rowCount);
}
}

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ApplicationRef, ComponentFactoryResolver, NgModule, Inject, forwardRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import { EditDataComponent, EDITDATA_SELECTOR } from 'sql/parts/grid/views/editData/editData.component';
import { SlickGrid } from 'angular2-slickgrid';
@NgModule({
imports: [
CommonModule,
BrowserModule
],
declarations: [
EditDataComponent,
SlickGrid
],
entryComponents: [
EditDataComponent
]
})
export class EditDataModule {
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService
) {
}
ngDoBootstrap(appRef: ApplicationRef) {
const factory = this._resolver.resolveComponentFactory(EditDataComponent);
const uniqueSelector: string = this._bootstrapService.getUniqueSelector(EDITDATA_SELECTOR);
(<any>factory).factory.selector = uniqueSelector;
appRef.bootstrap(factory);
}
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* 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 { IGridInfo } from 'sql/parts/grid/common/interfaces';
import { DataService } from 'sql/parts/grid/services/dataService';
import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, Action } from 'vs/base/common/actions';
export class EditDataGridActionProvider extends GridActionProvider {
constructor(dataService: DataService, selectAllCallback: (index: number) => void,
private _deleteRowCallback: (index: number) => void,
private _revertRowCallback: (index: number) => void) {
super(dataService, selectAllCallback);
}
/**
* Return actions given a click on an edit data grid
*/
public getGridActions(): TPromise<IAction[]> {
let actions: IAction[] = [];
actions.push(new DeleteRowAction(DeleteRowAction.ID, DeleteRowAction.LABEL, this._deleteRowCallback));
actions.push(new RevertRowAction(RevertRowAction.ID, RevertRowAction.LABEL, this._revertRowCallback));
return TPromise.as(actions);
}
}
export class DeleteRowAction extends Action {
public static ID = 'grid.deleteRow';
public static LABEL = localize('deleteRow', 'Delete Row');
constructor(
id: string,
label: string,
private callback: (index: number) => void
) {
super(id, label);
}
public run(gridInfo: IGridInfo): TPromise<boolean> {
this.callback(gridInfo.rowIndex);
return TPromise.as(true);
}
}
export class RevertRowAction extends Action {
public static ID = 'grid.revertRow';
public static LABEL = localize('revertRow', 'Revert Row');
constructor(
id: string,
label: string,
private callback: (index: number) => void
) {
super(id, label);
}
public run(gridInfo: IGridInfo): TPromise<boolean> {
this.callback(gridInfo.rowIndex);
return TPromise.as(true);
}
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.editdata-component * {
box-sizing: border-box;
}

View File

@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* 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 { IGridInfo, IRange, SaveFormat } from 'sql/parts/grid/common/interfaces';
import { DataService } from 'sql/parts/grid/services/dataService';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { IAction, Action } from 'vs/base/common/actions';
export const GRID_SAVECSV_ID = 'grid.saveAsCsv';
export const GRID_SAVEJSON_ID = 'grid.saveAsJson';
export const GRID_SAVEEXCEL_ID = 'grid.saveAsExcel';
export const GRID_COPY_ID = 'grid.copySelection';
export const GRID_COPYWITHHEADERS_ID = 'grid.copyWithHeaders';
export const GRID_SELECTALL_ID = 'grid.selectAll';
export const MESSAGES_SELECTALL_ID = 'grid.messages.selectAll';
export const MESSAGES_COPY_ID = 'grid.messages.copy';
export const TOGGLERESULTS_ID = 'grid.toggleResultPane';
export const TOGGLEMESSAGES_ID = 'grid.toggleMessagePane';
export class GridActionProvider {
constructor(protected _dataService: DataService, protected _selectAllCallback: (index: number) => void) {
}
/**
* Return actions given a click on a grid
*/
public getGridActions(): TPromise<IAction[]> {
let actions: IAction[] = [];
actions.push(new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveFormat.CSV, this._dataService));
actions.push(new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveFormat.JSON, this._dataService));
actions.push(new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveFormat.EXCEL, this._dataService));
actions.push(new SelectAllGridAction(SelectAllGridAction.ID, SelectAllGridAction.LABEL, this._selectAllCallback));
actions.push(new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false, this._dataService));
actions.push(new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true, this._dataService));
return TPromise.as(actions);
}
/**
* Return actions given a click on a messages pane
*/
public getMessagesActions(dataService: DataService, selectAllCallback: () => void): TPromise<IAction[]> {
let actions: IAction[] = [];
actions.push(new CopyMessagesAction(CopyMessagesAction.ID, CopyMessagesAction.LABEL));
actions.push(new SelectAllMessagesAction(SelectAllMessagesAction.ID, SelectAllMessagesAction.LABEL, selectAllCallback));
return TPromise.as(actions);
}
}
export class SaveResultAction extends Action {
public static SAVECSV_ID = GRID_SAVECSV_ID;
public static SAVECSV_LABEL = localize('saveAsCsv', 'Save As CSV');
public static SAVEJSON_ID = GRID_SAVEJSON_ID;
public static SAVEJSON_LABEL = localize('saveAsJson', 'Save As JSON');
public static SAVEEXCEL_ID = GRID_SAVEEXCEL_ID;
public static SAVEEXCEL_LABEL = localize('saveAsExcel', 'Save As Excel');
constructor(
id: string,
label: string,
private format: SaveFormat,
private dataService: DataService
) {
super(id, label);
}
public run(gridInfo: IGridInfo): TPromise<boolean> {
this.dataService.sendSaveRequest({
batchIndex: gridInfo.batchIndex,
resultSetNumber: gridInfo.resultSetNumber,
selection: gridInfo.selection,
format: this.format
});
return TPromise.as(true);
}
}
export class CopyResultAction extends Action {
public static COPY_ID = GRID_COPY_ID;
public static COPY_LABEL = localize('copySelection', 'Copy');
public static COPYWITHHEADERS_ID = GRID_COPYWITHHEADERS_ID;
public static COPYWITHHEADERS_LABEL = localize('copyWithHeaders', 'Copy With Headers');
constructor(
id: string,
label: string,
private copyHeader: boolean,
private dataService: DataService
) {
super(id, label);
}
public run(gridInfo: IGridInfo): TPromise<boolean> {
this.dataService.copyResults(gridInfo.selection, gridInfo.batchIndex, gridInfo.resultSetNumber, this.copyHeader);
return TPromise.as(true);
}
}
export class SelectAllGridAction extends Action {
public static ID = GRID_SELECTALL_ID;
public static LABEL = localize('selectAll', 'Select All');
constructor(
id: string,
label: string,
private selectAllCallback: (index: number) => void
) {
super(id, label);
}
public run(gridInfo: IGridInfo): TPromise<boolean> {
this.selectAllCallback(gridInfo.gridIndex);
return TPromise.as(true);
}
}
export class SelectAllMessagesAction extends Action {
public static ID = MESSAGES_SELECTALL_ID;
public static LABEL = localize('selectAll', 'Select All');
constructor(
id: string,
label: string,
private selectAllCallback: () => void
) {
super(id, label);
}
public run(): TPromise<boolean> {
this.selectAllCallback();
return TPromise.as(true);
}
}
export class CopyMessagesAction extends Action {
public static ID = MESSAGES_COPY_ID;
public static LABEL = localize('copyMessages', 'Copy');
constructor(
id: string,
label: string
) {
super(id, label);
}
public run(selectedRange: IRange): TPromise<boolean> {
let selectedText = selectedRange.text();
WorkbenchUtils.executeCopy(selectedText);
return TPromise.as(true);
}
}

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
import { IQueryModelService } from 'sql/parts/query/execution/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 { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
function runActionOnActiveResultsEditor (accessor: ServicesAccessor, eventName: string): void {
let editorService = accessor.get(IWorkbenchEditorService);
const candidates = [editorService.getActiveEditor(), ...editorService.getVisibleEditors()].filter(e => {
if (e) {
let id = e.getId();
if (id === QueryEditor.ID || id === EditDataEditor.ID) {
// This is a query or edit data editor
return true;
}
}
return false;
});
if (candidates.length > 0) {
let queryModelService: IQueryModelService = accessor.get(IQueryModelService);
let uri = (<any> candidates[0].input).uri;
queryModelService.sendGridContentEvent(uri, eventName);
}
}
export const copySelection = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.CopySelection);
};
export const copyMessagesSelection = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.CopyMessagesSelection);
};
export const copyWithHeaders = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.CopyWithHeaders);
};
export const toggleMessagePane = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.ToggleMessagePane);
};
export const toggleResultsPane = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.ToggleResultPane);
};
export const saveAsCsv = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsCsv);
};
export const saveAsJson = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsJSON);
};
export const saveAsExcel = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsExcel);
};
export const selectAll = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SelectAll);
};
export const selectAllMessages = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SelectAllMessages);
};

View File

@@ -0,0 +1,554 @@
/*---------------------------------------------------------------------------------------------
* 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/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 { Subscription, Subject } from 'rxjs/Rx';
import { ElementRef, QueryList, ChangeDetectorRef, ViewChildren } from '@angular/core';
import { IGridDataRow, ISlickRange, SlickGrid, FieldType } from 'angular2-slickgrid';
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
import * as Constants from 'sql/parts/query/common/constants';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import { IGridInfo, IRange, IGridDataSet, SaveFormat } from 'sql/parts/grid/common/interfaces';
import * as Utils from 'sql/parts/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';
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
import { ResultsVisibleContext, ResultsGridFocussedContext, ResultsMessagesFocussedContext } from 'sql/parts/query/common/queryContext';
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import * as rangy from 'sql/base/node/rangy';
import { error } from 'sql/base/common/log';
import { IAction } from 'vs/base/common/actions';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
import { DragCellSelectionModel } from 'sql/base/browser/ui/table/plugins/dragCellSelectionModel.plugin';
export abstract class GridParentComponent {
// CONSTANTS
// tslint:disable:no-unused-variable
protected get selectionModel(): DragCellSelectionModel<any> {
return new DragCellSelectionModel<any>();
}
protected get slickgridPlugins(): Array<any> {
return [
new AutoColumnSize<any>({})
];
}
protected _rowHeight = 29;
protected _defaultNumShowingRows = 8;
protected Constants = Constants;
protected LocalizedConstants = LocalizedConstants;
protected Utils = Utils;
// tslint:disable-next-line:no-unused-variable
protected startString = new Date().toLocaleTimeString();
protected shortcutfunc: { [name: string]: Function };
// tslint:enable
// FIELDS
// Service for interaction with the IQueryModel
protected dataService: DataService;
protected keybindingService: IKeybindingService;
protected scopedContextKeyService: IContextKeyService;
protected contextMenuService: IContextMenuService;
protected actionProvider: actions.GridActionProvider;
protected toDispose: IDisposable[];
// Context keys to set when keybindings are available
private resultsVisibleContextKey: IContextKey<boolean>;
private gridFocussedContextKey: IContextKey<boolean>;
private messagesFocussedContextKey: IContextKey<boolean>;
// All datasets
// Place holder data sets to buffer between data sets and rendered data sets
protected placeHolderDataSets: IGridDataSet[] = [];
// Datasets currently being rendered on the DOM
protected renderedDataSets: IGridDataSet[] = this.placeHolderDataSets;
protected resultActive = true;
protected _messageActive = true;
protected activeGrid = 0;
@ViewChildren('slickgrid') slickgrids: QueryList<SlickGrid>;
// Edit Data functions
public onCellEditEnd: (event: { row: number, column: number, newValue: any }) => void;
public onCellEditBegin: (event: { row: number, column: number }) => void;
public onRowEditBegin: (event: { row: number }) => void;
public onRowEditEnd: (event: { row: number }) => void;
public onIsCellEditValid: (row: number, column: number, newValue: any) => boolean;
public onIsColumnEditable: (column: number) => boolean;
public overrideCellFn: (rowNumber, columnId, value?, data?) => string;
public loadDataFunction: (offset: number, count: number) => Promise<IGridDataRow[]>;
set messageActive(input: boolean) {
this._messageActive = input;
if (this.resultActive) {
this.resizeGrids();
}
}
get messageActive(): boolean {
return this._messageActive;
}
constructor(
protected _el: ElementRef,
protected _cd: ChangeDetectorRef,
protected _bootstrapService: IBootstrapService
) {
this.toDispose = [];
}
protected baseInit(): void {
const self = this;
this.initShortcutsBase();
if (this._bootstrapService.configurationService) {
let sqlConfig = this._bootstrapService.configurationService.getConfiguration('sql');
if (sqlConfig) {
this._messageActive = sqlConfig['messagesDefaultOpen'];
}
}
this.subscribeWithDispose(this.dataService.gridContentObserver, (type) => {
switch (type) {
case GridContentEvents.RefreshContents:
self.refreshResultsets();
break;
case GridContentEvents.ResizeContents:
self.resizeGrids();
break;
case GridContentEvents.CopySelection:
self.copySelection();
break;
case GridContentEvents.CopyWithHeaders:
self.copyWithHeaders();
break;
case GridContentEvents.CopyMessagesSelection:
self.copyMessagesSelection();
break;
case GridContentEvents.ToggleResultPane:
self.toggleResultPane();
break;
case GridContentEvents.ToggleMessagePane:
self.toggleMessagePane();
break;
case GridContentEvents.SelectAll:
self.onSelectAllForActiveGrid();
break;
case GridContentEvents.SelectAllMessages:
self.selectAllMessages();
break;
case GridContentEvents.SaveAsCsv:
self.sendSaveRequest(SaveFormat.CSV);
break;
case GridContentEvents.SaveAsJSON:
self.sendSaveRequest(SaveFormat.JSON);
break;
case GridContentEvents.SaveAsExcel:
self.sendSaveRequest(SaveFormat.EXCEL);
break;
default:
error('Unexpected grid content event type "' + type + '" sent');
break;
}
});
this.contextMenuService = this._bootstrapService.contextMenuService;
this.keybindingService = this._bootstrapService.keybindingService;
this.bindKeys(this._bootstrapService.contextKeyService);
}
/*
* Add the subscription to the list of things to be disposed on destroy, or else on a new component init
* may get the "destroyed" object still getting called back.
*/
protected subscribeWithDispose<T>(subject: Subject<T>, event: (value: any) => void): void {
let sub: Subscription = subject.subscribe(event);
this.toDispose.push(toDisposableSubscription(sub));
}
private bindKeys(contextKeyService: IContextKeyService): void {
if (contextKeyService) {
let gridContextKeyService = this._bootstrapService.contextKeyService.createScoped(this._el.nativeElement);
this.toDispose.push(gridContextKeyService);
this.resultsVisibleContextKey = ResultsVisibleContext.bindTo(gridContextKeyService);
this.resultsVisibleContextKey.set(true);
this.gridFocussedContextKey = ResultsGridFocussedContext.bindTo(gridContextKeyService);
this.messagesFocussedContextKey = ResultsMessagesFocussedContext.bindTo(gridContextKeyService);
}
}
protected baseDestroy(): void {
this.toDispose = dispose(this.toDispose);
}
private toggleResultPane(): void {
this.resultActive = !this.resultActive;
}
private toggleMessagePane(): void {
this.messageActive = !this.messageActive;
}
protected onGridFocus() {
this.gridFocussedContextKey.set(true);
}
protected onGridFocusout() {
this.gridFocussedContextKey.set(false);
}
protected onMessagesFocus() {
this.messagesFocussedContextKey.set(true);
}
protected onMessagesFocusout() {
this.messagesFocussedContextKey.set(false);
}
private copySelection(): void {
let messageText = this.getMessageText();
if (messageText.length > 0) {
WorkbenchUtils.executeCopy(messageText);
} else {
let activeGrid = this.activeGrid;
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
this.dataService.copyResults(selection, this.renderedDataSets[activeGrid].batchId, this.renderedDataSets[activeGrid].resultId);
}
}
private copyWithHeaders(): void {
let activeGrid = this.activeGrid;
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
this.dataService.copyResults(selection, this.renderedDataSets[activeGrid].batchId,
this.renderedDataSets[activeGrid].resultId, true);
}
private copyMessagesSelection(): void {
let messageText = this.getMessageText();
if (messageText.length === 0) {
// Since we know we're specifically copying messages, do a select all if nothing is selected
this.selectAllMessages();
messageText = this.getMessageText();
}
if (messageText.length > 0) {
WorkbenchUtils.executeCopy(messageText);
}
}
private getMessageText(): string {
let range: IRange = this.getSelectedRangeUnderMessages();
return range ? range.text() : '';
}
private initShortcutsBase(): void {
let shortcuts = {
'ToggleResultPane': () => {
this.toggleResultPane();
},
'ToggleMessagePane': () => {
this.toggleMessagePane();
},
'CopySelection': () => {
this.copySelection();
},
'CopyWithHeaders': () => {
this.copyWithHeaders();
},
'SelectAll': () => {
this.onSelectAllForActiveGrid();
},
'SaveAsCSV': () => {
this.sendSaveRequest(SaveFormat.CSV);
},
'SaveAsJSON': () => {
this.sendSaveRequest(SaveFormat.JSON);
},
'SaveAsExcel': () => {
this.sendSaveRequest(SaveFormat.EXCEL);
}
};
this.initShortcuts(shortcuts);
this.shortcutfunc = shortcuts;
}
protected abstract initShortcuts(shortcuts: { [name: string]: Function }): void;
/**
* Send save result set request to service
*/
handleContextClick(event: { type: string, batchId: number, resultId: number, index: number, selection: ISlickRange[] }): void {
switch (event.type) {
case 'savecsv':
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.CSV, selection: event.selection });
break;
case 'savejson':
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.JSON, selection: event.selection });
break;
case 'saveexcel':
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.EXCEL, selection: event.selection });
break;
case 'selectall':
this.activeGrid = event.index;
this.onSelectAllForActiveGrid();
break;
case 'copySelection':
this.dataService.copyResults(event.selection, event.batchId, event.resultId);
break;
case 'copyWithHeaders':
this.dataService.copyResults(event.selection, event.batchId, event.resultId, true);
break;
default:
break;
}
}
private sendSaveRequest(format: SaveFormat) {
let activeGrid = this.activeGrid;
let batchId = this.renderedDataSets[activeGrid].batchId;
let resultId = this.renderedDataSets[activeGrid].resultId;
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
this.dataService.sendSaveRequest({ batchIndex: batchId, resultSetNumber: resultId, format: format, selection: selection });
}
protected _keybindingFor(action: IAction): ResolvedKeybinding {
var [kb] = this.keybindingService.lookupKeybindings(action.id);
return kb;
}
openContextMenu(event, batchId, resultId, index): void {
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
let slick: any = this.slickgrids.toArray()[index];
let grid = slick._grid;
let rowIndex = grid.getCellFromEvent(event).row;
let actionContext: IGridInfo = {
batchIndex: batchId,
resultSetNumber: resultId,
selection: selection,
gridIndex: index,
rowIndex: rowIndex
};
let anchor = { x: event.pageX + 1, y: event.pageY };
this.contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => this.actionProvider.getGridActions(),
getKeyBinding: (action) => this._keybindingFor(action),
onHide: (wasCancelled?: boolean) => {
},
getActionsContext: () => (actionContext)
});
}
/**
* Returns a function that selects all elements of a grid. This needs to
* return a function in order to capture the scope for this component
* @private
* @returns {(gridIndex: number) => void}
*
* @memberOf QueryComponent
*/
protected onGridSelectAll(): (gridIndex: number) => void {
let self = this;
return (gridIndex: number) => {
self.activeGrid = gridIndex;
self.slickgrids.toArray()[this.activeGrid].selection = true;
};
}
private onSelectAllForActiveGrid(): void {
if (this.activeGrid >= 0 && this.slickgrids.length > this.activeGrid) {
this.slickgrids.toArray()[this.activeGrid].selection = true;
}
}
/**
* Used to convert the string to a enum compatible with SlickGrid
*/
protected stringToFieldType(input: string): FieldType {
let fieldtype: FieldType;
switch (input) {
case 'string':
fieldtype = FieldType.String;
break;
default:
fieldtype = FieldType.String;
break;
}
return fieldtype;
}
/**
* Makes a resultset take up the full result height if this is not already true
* Otherwise rerenders the result sets from default
*/
magnify(index: number): void {
const self = this;
if (this.renderedDataSets.length > 1) {
this.renderedDataSets = [this.placeHolderDataSets[index]];
} else {
this.renderedDataSets = this.placeHolderDataSets;
this.onScroll(0);
}
setTimeout(() => {
self.resizeGrids();
self.slickgrids.toArray()[0].setActive();
self._cd.detectChanges();
});
}
abstract onScroll(scrollTop): void;
protected getResultsElement(): any {
return this._el.nativeElement.querySelector('#results');
}
protected getMessagesElement(): any {
return this._el.nativeElement.querySelector('#messages');
}
/**
* Force angular to re-render the results grids. Calling this upon unhide (upon focus) fixes UI
* glitches that occur when a QueryRestulsEditor is hidden then unhidden while it is running a query.
*/
refreshResultsets(): void {
let tempRenderedDataSets = this.renderedDataSets;
this.renderedDataSets = [];
this._cd.detectChanges();
this.renderedDataSets = tempRenderedDataSets;
this._cd.detectChanges();
}
getSelectedRangeUnderMessages(): IRange {
let selectedRange: IRange = undefined;
let msgEl = this.getMessagesElement();
if (msgEl) {
selectedRange = this.getSelectedRangeWithin(msgEl);
}
return selectedRange;
}
getSelectedRangeWithin(el): IRange {
let selectedRange = undefined;
let sel = rangy.getSelection();
let elRange = <IRange>rangy.createRange();
elRange.selectNodeContents(el);
if (sel.rangeCount) {
selectedRange = sel.getRangeAt(0).intersection(elRange);
}
elRange.detach();
return selectedRange;
}
selectAllMessages(): void {
let msgEl = this._el.nativeElement.querySelector('#messages');
this.selectElementContents(msgEl);
}
selectElementContents(el): void {
let range = rangy.createRange();
range.selectNodeContents(el);
let sel = rangy.getSelection();
sel.setSingleRange(range);
}
/**
* Add handler for clicking on xml link
*/
xmlLinkHandler = (cellRef: string, row: number, dataContext: JSON, colDef: any) => {
const self = this;
self.handleLink(cellRef, row, dataContext, colDef, 'xml');
}
/**
* Add handler for clicking on json link
*/
jsonLinkHandler = (cellRef: string, row: number, dataContext: JSON, colDef: any) => {
const self = this;
self.handleLink(cellRef, row, dataContext, colDef, 'json');
}
private handleLink(cellRef: string, row: number, dataContext: JSON, colDef: any, linkType: string): void {
const self = this;
let value = self.getCellValueString(dataContext, colDef);
$(cellRef).children('.xmlLink').click(function (): void {
self.dataService.openLink(value, colDef.name, linkType);
});
}
private getCellValueString(dataContext: JSON, colDef: any): string {
let returnVal = '';
let value = dataContext[colDef.field];
if (Services.DBCellValue.isDBCellValue(value)) {
returnVal = value.displayValue;
} else if (typeof value === 'string') {
returnVal = value;
}
return returnVal;
}
/**
* Return asyncPostRender handler based on type
*/
public linkHandler(type: string): Function {
if (type === 'xml') {
return this.xmlLinkHandler;
} else { // default to JSON handler
return this.jsonLinkHandler;
}
}
keyEvent(e: KeyboardEvent): void {
let self = this;
let handled = self.tryHandleKeyEvent(e);
if (handled) {
e.preventDefault();
e.stopPropagation();
}
// Else assume that keybinding service handles routing this to a command
}
/**
* Called by keyEvent method to give child classes a chance to
* handle key events.
*
* @protected
* @abstract
* @param {any} e
* @returns {boolean}
*
* @memberOf GridParentComponent
*/
protected abstract tryHandleKeyEvent(e): boolean;
resizeGrids(): void {
const self = this;
setTimeout(() => {
for (let grid of self.renderedDataSets) {
grid.resized.emit();
}
});
}
// Private Helper Functions ////////////////////////////////////////////////////////////////////////////
}

View File

@@ -0,0 +1,57 @@
<div #taskbarContainer></div>
<div style="display: flex; flex-flow: row; overflow: scroll; height: 100%; width: 100%">
<div style="flex:3 3 auto; margin: 5px">
<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">{{chartTypeLabel}}</div>
<div class="input-divider" #chartTypesContainer></div>
<div [hidden]="chartTypesSelectBox.value === 'count' || chartTypesSelectBox.value === 'image'">
<div [hidden]="!showDataType">
<div class="dialog-label">{{dataTypeLabel}}</div>
<div class="radio-indent">
<div class="option">
<input type="radio" name=data-type value="number" [(ngModel)]="dataType">{{numberLabel}}
</div>
<div class="option">
<input type="radio" name=data-type value="point" [(ngModel)]="dataType">{{pointLabel}}
</div>
</div>
</div>
<div [hidden]="!showDataDirection">
<div class="dialog-label">{{dataDirectionLabel}}</div>
<div class="radio-indent">
<div class="option">
<input type="radio" name=data-direction value="vertical" [(ngModel)]="dataDirection">{{verticalLabel}}
</div>
<div class="option">
<input type="radio" name=data-direction value="horizontal" [(ngModel)]="dataDirection">{{horizontalLabel}}
</div>
</div>
</div>
<div [hidden]="!showLabelFirstColumn">
<div class="input-divider" #labelFirstColumnContainer></div>
</div>
<div [hidden]="!showColumnsAsLabels">
<div class="input-divider" #columnsAsLabelsContainer></div>
</div>
<div class="dialog-label">{{legendLabel}}</div>
<div class="input-divider" #legendContainer></div>
<div class="footer">
<div class="right-footer">
<div class="footer-button" #createInsightButtonContainer></div>
<div class="footer-button" #saveChartButtonContainer></div>
<div class="footer-button" #copyChartButtonContainer></div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,347 @@
/*---------------------------------------------------------------------------------------------
* 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, ViewContainerRef, 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 { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
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 { DataType, 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';
/* Insights */
import {
ChartInsight, DataDirection, LegendPosition
} from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Builder } from 'vs/base/browser/builder';
import { attachSelectBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
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';
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
@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[];
private chartTypesSelectBox: SelectBox;
private legendSelectBox: SelectBox;
private labelFirstColumnCheckBox: Checkbox;
private columnsAsLabelsCheckBox: Checkbox;
/* UI */
/* tslint:disable:no-unused-variable */
private chartTypeLabel: string = nls.localize('chartTypeLabel', 'Chart Type');
private dataDirectionLabel: string = nls.localize('dataDirectionLabel', 'Data Direction');
private verticalLabel: string = nls.localize('verticalLabel', 'Vertical');
private horizontalLabel: string = nls.localize('horizontalLabel', 'Horizontal');
private dataTypeLabel: string = nls.localize('dataTypeLabel', 'Data Type');
private numberLabel: string = nls.localize('numberLabel', 'Number');
private pointLabel: string = nls.localize('pointLabel', 'Point');
private labelFirstColumnLabel: string = nls.localize('labelFirstColumnLabel', 'Use First Column as row label?');
private columnsAsLabelsLabel: string = nls.localize('columnsAsLabelsLabel', 'Use Column names as labels?');
private legendLabel: string = nls.localize('legendLabel', 'Legend Position');
private chartNotFoundError: string = nls.localize('chartNotFound', 'Could not find chart to save');
/* tslint:enable:no-unused-variable */
private _actionBar: Taskbar;
private _createInsightAction: CreateInsightAction;
private _copyAction: CopyAction;
private _saveAction: SaveImageAction;
private _chartConfig: ILineConfig;
private _disposables: Array<IDisposable> = [];
private _dataSet: IGridDataSet;
private _executeResult: IInsightData;
private _chartComponent: ChartInsight;
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
@ViewChild('taskbarContainer', { read: ElementRef }) private taskbarContainer;
@ViewChild('chartTypesContainer', { read: ElementRef }) private chartTypesElement;
@ViewChild('legendContainer', { read: ElementRef }) private legendElement;
@ViewChild('labelFirstColumnContainer', { read: ElementRef }) private labelFirstColumnElement;
@ViewChild('columnsAsLabelsContainer', { read: ElementRef }) private columnsAsLabelsElement;
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
@Inject(forwardRef(() => ViewContainerRef)) private _viewContainerRef: ViewContainerRef,
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
) {
}
ngOnInit() {
this._chartConfig = <ILineConfig>{
dataDirection: 'vertical',
dataType: 'number',
legendPosition: 'none',
labelFirstColumn: false
};
this.legendOptions = Object.values(LegendPosition);
this.initializeUI();
}
private initializeUI() {
// Initialize the taskbar
this._initActionBar();
// Init chart type dropdown
this.chartTypesSelectBox = new SelectBox(insightRegistry.getAllIds(), 'horizontalBar');
this.chartTypesSelectBox.render(this.chartTypesElement.nativeElement);
this.chartTypesSelectBox.onDidSelect(selected => this.onChartChanged());
this._disposables.push(attachSelectBoxStyler(this.chartTypesSelectBox, this._bootstrapService.themeService));
// Init label first column checkbox
// Note: must use 'self' for callback
this.labelFirstColumnCheckBox = DialogHelper.createCheckBox(new Builder(this.labelFirstColumnElement.nativeElement),
this.labelFirstColumnLabel, 'chartView-checkbox', false, () => this.onLabelFirstColumnChanged());
this._disposables.push(attachCheckboxStyler(this.labelFirstColumnCheckBox, this._bootstrapService.themeService));
// Init label first column checkbox
// Note: must use 'self' for callback
this.columnsAsLabelsCheckBox = DialogHelper.createCheckBox(new Builder(this.columnsAsLabelsElement.nativeElement),
this.columnsAsLabelsLabel, 'chartView-checkbox', false, () => this.columnsAsLabelsChanged());
this._disposables.push(attachCheckboxStyler(this.columnsAsLabelsCheckBox, this._bootstrapService.themeService));
// Init legend dropdown
this.legendSelectBox = new SelectBox(this.legendOptions, this._chartConfig.legendPosition);
this.legendSelectBox.render(this.legendElement.nativeElement);
this.legendSelectBox.onDidSelect(selected => this.onLegendChanged());
this._disposables.push(attachSelectBoxStyler(this.legendSelectBox, this._bootstrapService.themeService));
}
private _initActionBar() {
this._createInsightAction = this._bootstrapService.instantiationService.createInstance(CreateInsightAction);
this._copyAction = this._bootstrapService.instantiationService.createInstance(CopyAction);
this._saveAction = this._bootstrapService.instantiationService.createInstance(SaveImageAction);
let taskbar = <HTMLElement>this.taskbarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar, this._bootstrapService.contextMenuService);
this._actionBar.context = this;
this._actionBar.setContent([
{ action: this._createInsightAction },
{ action: this._copyAction },
{ action: this._saveAction }
]);
}
public onChartChanged(): void {
if (['scatter', 'timeSeries'].some(item => item === this.chartTypesSelectBox.value)) {
this.dataType = DataType.Point;
this.dataDirection = DataDirection.Horizontal;
}
this.initChart();
}
public onLabelFirstColumnChanged(): void {
this._chartConfig.labelFirstColumn = this.labelFirstColumnCheckBox.checked;
this.initChart();
}
public columnsAsLabelsChanged(): void {
this._chartConfig.columnsAsLabels = this.columnsAsLabelsCheckBox.checked;
this.initChart();
}
public onLegendChanged(): void {
this._chartConfig.legendPosition = <LegendPosition>this.legendSelectBox.value;
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(this.chartNotFoundError);
return;
}
this._bootstrapService.clipboardService.writeImageDataUrl(data);
}
public saveChart(): void {
let filePath = this.promptForFilepath();
let data = this._chartComponent.getCanvasData();
if (!data) {
this.showError(this.chartNotFoundError);
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._bootstrapService.windowsService.openExternal(fileUri.toString());
this._bootstrapService.messageService.show(Severity.Info, nls.localize('chartSaved', 'Saved Chart to path: {0}', filePath));
}
});
}
}
private promptForFilepath(): string {
let filepathPlaceHolder = PathUtilities.resolveCurrentDirectory(this.getActiveUriString(), PathUtilities.getRootPath(this._bootstrapService.workspaceContextService));
filepathPlaceHolder = paths.join(filepathPlaceHolder, 'chart.png');
let filePath: string = this._bootstrapService.windowService.showSaveDialog({
title: nls.localize('saveAsFileTitle', 'Choose Results File'),
defaultPath: paths.normalize(filepathPlaceHolder, true)
});
return filePath;
}
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._bootstrapService.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._bootstrapService.messageService.show(Severity.Error, errorMsg);
}
private getGridItemConfig(): NgGridItemConfig {
let config: NgGridItemConfig = {
sizex: 2,
sizey: 1
};
return config;
}
private getActiveUriString(): string {
let editorService = this._bootstrapService.editorService;
let editor = editorService.getActiveEditor();
if (editor && editor instanceof QueryEditor) {
let queryEditor: QueryEditor = editor;
return queryEditor.uri;
}
return undefined;
}
private get showDataDirection(): boolean {
return ['pie', 'horizontalBar', 'bar', 'doughnut'].some(item => item === this.chartTypesSelectBox.value) || (this.chartTypesSelectBox.value === 'line' && this.dataType === 'number');
}
private get showLabelFirstColumn(): boolean {
return this.dataDirection === 'horizontal' && this.dataType !== 'point';
}
private get showColumnsAsLabels(): boolean {
return this.dataDirection === 'vertical' && this.dataType !== 'point';
}
private get showDataType(): boolean {
return this.chartTypesSelectBox.value === 'line';
}
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._dataSet = dataSet;
this._executeResult = <IInsightData>{};
this._executeResult.columns = dataSet.columnDefinitions.map(def => def.name);
this._executeResult.rows = dataSet.dataRows.getRange(0, dataSet.dataRows.getLength()).map(gridRow => {
return gridRow.values.map(cell => cell.displayValue);
});
this.initChart();
}
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;
this._chartComponent.config = 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

@@ -0,0 +1,56 @@
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

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TPromise } from 'vs/base/common/winjs.base';
import { Action } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
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 messageService: IMessageService
) {
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.messageService.show(Severity.Error, 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(@IMessageService messageService: IMessageService
) {
super(CreateInsightAction.ID, CreateInsightAction.LABEL, 'createInsight', messageService);
}
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(@IMessageService messageService: IMessageService
) {
super(CopyAction.ID, CopyAction.LABEL, 'copyImage', messageService);
}
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(@IMessageService messageService: IMessageService
) {
super(SaveImageAction.ID, SaveImageAction.LABEL, 'saveAsImage', messageService);
}
public run(context: IChartViewActionContext): TPromise<boolean> {
return this.doRun(context, () => context.saveChart());
}
}

View File

@@ -0,0 +1,79 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="fullsize vertBox" style="position: relative">
<div #resultsPane *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 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}}" [columnDefinitions]="dataSet.columnDefinitions"
[ngClass]="i === activeGrid ? 'active' : ''"
[dataRows]="dataSet.dataRows"
(contextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)"
enableAsyncPostRender="true"
showDataTypeIcon="false"
showHeader="true"
[resized]="dataSet.resized"
(mousedown)="navigateToGrid(i)"
[selectionModel]="selectionModel"
[plugins]="slickgridPlugins"
class="boxCol content vertBox slickgrid">
</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;"
(click)="icon.functionality(dataSet.batchId, dataSet.resultId, i)"
[title]="icon.hoverText()" [ngClass]="icon.icon()">
</a>
</div>
</div>
</span>
</div>
</div>
<div id="messagepane" 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 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 class="queryLink" *ngIf="message.link" (click)="onSelectionLinkClicked(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

@@ -0,0 +1,603 @@
/*---------------------------------------------------------------------------------------------
* 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 rangy from 'sql/base/node/rangy';
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
import * as Services from 'sql/parts/grid/services/sharedServices';
import { IGridIcon, IMessage, IRange, IGridDataSet } from 'sql/parts/grid/common/interfaces';
import { GridParentComponent } from 'sql/parts/grid/views/gridParentComponent';
import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
import { QueryComponentParams } from 'sql/services/bootstrap/bootstrapParams';
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
import { error } from 'sql/base/common/log';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import * as strings from 'vs/base/common/strings';
import { clone } from 'vs/base/common/objects';
import * as DOM from 'vs/base/browser/dom';
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
private stringsFormat: any = strings.format;
// 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.slickgrids.toArray()[index].getSelectedRanges();
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.slickgrids.toArray()[index].getSelectedRanges();
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.slickgrids.toArray()[index].getSelectedRanges();
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 '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 scrollTimeOut: number;
private resizing = false;
private resizeHandleTop: string = '0';
private scrollEnabled = true;
// 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;
public queryExecutionStatus: EventEmitter<string> = new EventEmitter<string>();
public queryPlanAvailable: EventEmitter<string> = new EventEmitter<string>();
public showChartRequested: EventEmitter<IGridDataSet> = new EventEmitter<IGridDataSet>();
@Input() public queryParameters: QueryComponentParams;
@ViewChildren('slickgrid') slickgrids: QueryList<SlickGrid>;
// tslint:disable-next-line:no-unused-variable
@ViewChild('resultsPane', { read: ElementRef }) private _resultsPane: ElementRef;
constructor(
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
@Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService
) {
super(el, cd, bootstrapService);
this._el.nativeElement.className = 'slickgridContainer';
}
/**
* Called by Angular when the object is initialized
*/
ngOnInit(): void {
const self = this;
this.dataService = this.queryParameters.dataService;
this.actionProvider = new 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.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;
// 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);
clearTimeout(self.messageTimeout);
self.messageTimeout = setTimeout(() => {
self.messages = self.messages.concat(self.messageStore);
self.messageStore = [];
self._cd.detectChanges();
self.scrollMessages();
}, 10);
}
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: rows.rows[row]
});
}
// if this is a query plan resultset we haven't processed yet then forward to subscribers
if (self.hasQueryPlan && !self.sentPlans[resultSet.batchId]) {
self.sentPlans[resultSet.batchId] = 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';
}
// 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: 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'
: c.columnName,
type: self.stringToFieldType('string'),
formatter: isLinked ? Services.hyperLinkFormatter : Services.textFormatter,
asyncPostRender: isLinked ? self.linkHandler(linkType) : undefined
};
})
};
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;
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);
}
/**
* Perform copy and do other actions for context menu on the messages component
*/
handleMessagesContextClick(event: { type: string, selectedRange: IRange }): void {
switch (event.type) {
case 'copySelection':
let selectedText = event.selectedRange.text();
WorkbenchUtils.executeCopy(selectedText);
break;
case 'selectall':
document.execCommand('selectAll');
break;
default:
break;
}
}
openMessagesContextMenu(event: any): void {
let self = this;
event.preventDefault();
let selectedRange: IRange = 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();
if (self.firstRender) {
let setActive = () => {
if (self.firstRender && self.slickgrids.toArray().length > 0) {
self.slickgrids.toArray()[0].setActive();
self.firstRender = false;
}
};
setTimeout(() => {
setActive();
});
}
}, self.scrollTimeOutTime);
}
onSelectionLinkClicked(index: number): void {
this.dataService.setEditorSelection(index);
}
/**
* 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 || targetIndex < 0 || !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
rangy.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();
}
});
}
private showChartForGrid(index: number) {
if (this.renderedDataSets.length > index) {
this.showChartRequested.emit(this.renderedDataSets[index]);
}
}
/* Helper function to toggle messages and results panes */
// tslint:disable-next-line:no-unused-variable
private togglePane(pane: PaneType): void {
if (pane === 'messages') {
this._messageActive = !this._messageActive;
} else if (pane === 'results') {
this.resultActive = !this.resultActive;
}
this._cd.detectChanges();
this.resizeGrids();
}
layout() {
this.resizeGrids();
}
}