mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-04 09:35:38 -05:00
Refactor results grid (#2147)
* got a basic ui * working on message panel * done with messages moving to grids * formatting * working on multiple grids * it does work * styling * formatting * formatting * fixed reset methods * moved for scrollable * formatting * fixing scrolling * making progress * formatting * fixed scrolling * fix horizontal scrolling and size * fix columns for tables * integrate view item * implementing heightmap scrolling * add context menu and fix scrolling * formatting * revert slickgrid * add actions to message pane * formatting * formatting * bottom padding for tables * minimized and maximized table actions * add timestamp * added batch start message with selection * updating * formatting * formatting * fix execution time * formatting * fix problems * fix rendering issues, add icons * formatting * formatting * added commit change * fix performance, message scrolling, etc * formatting * formatting * fixing performance * formatting * update package * tring to fix bugs * reworking * the problem is the 1st sash is always the first sash visible * remove resizing from grid panels * add missing files * trying to get edit to work * fix editdata * formatting * update angular2-slickgrid
This commit is contained in:
@@ -26,7 +26,6 @@ import * as paths from 'vs/base/common/paths';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as pretty from 'pretty-data';
|
||||
|
||||
import { ISlickRange } from 'angular2-slickgrid';
|
||||
import * as path from 'path';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
@@ -255,7 +254,7 @@ export class ResultSerializer {
|
||||
return config;
|
||||
}
|
||||
|
||||
private getParameters(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: ISlickRange): SaveResultsRequestParams {
|
||||
private getParameters(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): SaveResultsRequestParams {
|
||||
let saveResultsParams: SaveResultsRequestParams;
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
this._filePath = PathUtilities.resolveFilePath(this._uri, filePath, this.rootPath);
|
||||
@@ -287,7 +286,7 @@ export class ResultSerializer {
|
||||
/**
|
||||
* Check if a range of cells were selected.
|
||||
*/
|
||||
private isSelected(selection: ISlickRange): boolean {
|
||||
private isSelected(selection: Slick.Range): boolean {
|
||||
return (selection && !((selection.fromCell === selection.toCell) && (selection.fromRow === selection.toRow)));
|
||||
}
|
||||
|
||||
@@ -319,7 +318,7 @@ export class ResultSerializer {
|
||||
/**
|
||||
* Send request to sql tools service to save a result set
|
||||
*/
|
||||
private sendRequestToService(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: ISlickRange): Thenable<void> {
|
||||
private sendRequestToService(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): Thenable<void> {
|
||||
let saveResultsParams = this.getParameters(filePath, batchIndex, resultSetNo, format, selection);
|
||||
|
||||
this.logToOutputChannel(LocalizedConstants.msgSaveStarted + this._filePath);
|
||||
|
||||
160
src/sql/parts/query/editor/actions.ts
Normal file
160
src/sql/parts/query/editor/actions.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Action } from 'vs/base/common/actions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
|
||||
import QueryRunner from 'sql/parts/query/execution/queryRunner';
|
||||
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { GridTableState } from 'sql/parts/query/editor/gridPanel';
|
||||
|
||||
export interface IGridActionContext {
|
||||
cell: { row: number; cell: number; };
|
||||
selection: Slick.Range[];
|
||||
runner: QueryRunner;
|
||||
batchId: number;
|
||||
resultId: number;
|
||||
table: Table<any>;
|
||||
tableState: GridTableState;
|
||||
}
|
||||
|
||||
export interface IMessagesActionContext {
|
||||
selection: Selection;
|
||||
tree: ITree;
|
||||
}
|
||||
|
||||
export class SaveResultAction extends Action {
|
||||
public static SAVECSV_ID = 'grid.saveAsCsv';
|
||||
public static SAVECSV_LABEL = localize('saveAsCsv', 'Save As CSV');
|
||||
public static SAVECSV_ICON = 'saveCsv';
|
||||
|
||||
public static SAVEJSON_ID = 'grid.saveAsJson';
|
||||
public static SAVEJSON_LABEL = localize('saveAsJson', 'Save As JSON');
|
||||
public static SAVEJSON_ICON = 'saveJson';
|
||||
|
||||
public static SAVEEXCEL_ID = 'grid.saveAsExcel';
|
||||
public static SAVEEXCEL_LABEL = localize('saveAsExcel', 'Save As Excel');
|
||||
public static SAVEEXCEL_ICON = 'saveExcel';
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
icon: string,
|
||||
private format: SaveFormat
|
||||
) {
|
||||
super(id, label, icon);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): TPromise<boolean> {
|
||||
context.runner.serializeResults(context.batchId, context.resultId, this.format, context.selection);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyResultAction extends Action {
|
||||
public static COPY_ID = 'grid.copySelection';
|
||||
public static COPY_LABEL = localize('copySelection', 'Copy');
|
||||
|
||||
public static COPYWITHHEADERS_ID = 'grid.copyWithHeaders';
|
||||
public static COPYWITHHEADERS_LABEL = localize('copyWithHeaders', 'Copy With Headers');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private copyHeader: boolean,
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): TPromise<boolean> {
|
||||
context.runner.copyResults(context.selection, context.batchId, context.resultId, this.copyHeader);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectAllGridAction extends Action {
|
||||
public static ID = 'grid.selectAll';
|
||||
public static LABEL = localize('selectAll', 'Select All');
|
||||
|
||||
constructor() {
|
||||
super(SelectAllGridAction.ID, SelectAllGridAction.LABEL);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): TPromise<boolean> {
|
||||
context.table.setSelectedRows(true);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyMessagesAction extends Action {
|
||||
public static ID = 'grid.messages.copy';
|
||||
public static LABEL = localize('copyMessages', 'Copy');
|
||||
|
||||
constructor(
|
||||
@IClipboardService private clipboardService: IClipboardService
|
||||
) {
|
||||
super(CopyMessagesAction.ID, CopyMessagesAction.LABEL);
|
||||
}
|
||||
|
||||
public run(context: IMessagesActionContext): TPromise<boolean> {
|
||||
this.clipboardService.writeText(context.selection.toString());
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectAllMessagesAction extends Action {
|
||||
public static ID = 'grid.messages.selectAll';
|
||||
public static LABEL = localize('selectAll', 'Select All');
|
||||
|
||||
constructor() {
|
||||
super(SelectAllMessagesAction.ID, SelectAllMessagesAction.LABEL);
|
||||
}
|
||||
|
||||
public run(context: IMessagesActionContext): TPromise<boolean> {
|
||||
let range = document.createRange();
|
||||
range.selectNodeContents(context.tree.getHTMLElement());
|
||||
let sel = document.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaximizeTableAction extends Action {
|
||||
public static ID = 'grid.maximize';
|
||||
public static LABEL = localize('maximize', 'Maximize');
|
||||
public static ICON = 'extendFullScreen';
|
||||
|
||||
constructor() {
|
||||
super(MaximizeTableAction.ID, MaximizeTableAction.LABEL, MaximizeTableAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): TPromise<boolean> {
|
||||
context.tableState.maximized = true;
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class MinimizeTableAction extends Action {
|
||||
public static ID = 'grid.minimize';
|
||||
public static LABEL = localize('minimize', 'Minimize');
|
||||
public static ICON = 'exitFullScreen';
|
||||
|
||||
constructor() {
|
||||
super(MinimizeTableAction.ID, MinimizeTableAction.LABEL, MinimizeTableAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): TPromise<boolean> {
|
||||
context.tableState.maximized = false;
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
414
src/sql/parts/query/editor/gridPanel.ts
Normal file
414
src/sql/parts/query/editor/gridPanel.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { attachTableStyler } from 'sql/common/theme/styler';
|
||||
import QueryRunner from 'sql/parts/query/execution/queryRunner';
|
||||
import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
|
||||
import { Table, ITableStyles, ITableContextMenuEvent } from 'sql/base/browser/ui/table/table';
|
||||
import { ScrollableSplitView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview';
|
||||
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
|
||||
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
||||
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
|
||||
import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, MinimizeTableAction } from 'sql/parts/query/editor/actions';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import { Orientation, IView } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { $ } from 'vs/base/browser/builder';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Separator, ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Dimension, getContentWidth } from 'vs/base/browser/dom';
|
||||
|
||||
const rowHeight = 29;
|
||||
const columnHeight = 26;
|
||||
const minGridHeightInRows = 8;
|
||||
const estimatedScrollBarHeight = 10;
|
||||
|
||||
export interface IGridTableState {
|
||||
canBeMaximized: boolean;
|
||||
maximized: boolean;
|
||||
}
|
||||
|
||||
export class GridTableState {
|
||||
|
||||
private _maximized: boolean;
|
||||
|
||||
private _onMaximizedChange = new Emitter<boolean>();
|
||||
public onMaximizedChange: Event<boolean> = this._onMaximizedChange.event;
|
||||
|
||||
public canBeMaximized: boolean;
|
||||
|
||||
constructor(state?: IGridTableState) {
|
||||
if (state) {
|
||||
this._maximized = state.maximized;
|
||||
this.canBeMaximized = state.canBeMaximized;
|
||||
}
|
||||
}
|
||||
|
||||
public get maximized(): boolean {
|
||||
return this._maximized;
|
||||
}
|
||||
|
||||
public set maximized(val: boolean) {
|
||||
if (val === this._maximized) {
|
||||
return;
|
||||
}
|
||||
this._maximized = val;
|
||||
this._onMaximizedChange.fire(val);
|
||||
}
|
||||
|
||||
public clone(): GridTableState {
|
||||
return new GridTableState({ canBeMaximized: this.canBeMaximized, maximized: this.maximized });
|
||||
}
|
||||
}
|
||||
|
||||
export class GridPanel extends ViewletPanel {
|
||||
private container = document.createElement('div');
|
||||
private splitView: ScrollableSplitView;
|
||||
private tables: GridTable<any>[] = [];
|
||||
private tableDisposable: IDisposable[] = [];
|
||||
private queryRunnerDisposables: IDisposable[] = [];
|
||||
|
||||
private runner: QueryRunner;
|
||||
|
||||
private maximizedGrid: GridTable<any>;
|
||||
|
||||
constructor(
|
||||
title: string, options: IViewletPanelOptions,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IThemeService private themeService: IThemeService
|
||||
) {
|
||||
super(title, options, keybindingService, contextMenuService, configurationService);
|
||||
this.splitView = new ScrollableSplitView(this.container, { enableResizing: false });
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this.container.style.width = '100%';
|
||||
this.container.style.height = '100%';
|
||||
|
||||
container.appendChild(this.container);
|
||||
}
|
||||
|
||||
protected layoutBody(size: number): void {
|
||||
this.splitView.layout(size);
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
dispose(this.queryRunnerDisposables);
|
||||
this.reset();
|
||||
this.queryRunnerDisposables = [];
|
||||
this.runner = runner;
|
||||
this.queryRunnerDisposables.push(this.runner.onResultSet(e => this.onResultSet(e)));
|
||||
this.queryRunnerDisposables.push(this.runner.onQueryStart(() => this.reset()));
|
||||
}
|
||||
|
||||
private onResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
this.addResultSet(resultSet);
|
||||
|
||||
this.tables.map(t => {
|
||||
t.state.canBeMaximized = this.tables.length > 1;
|
||||
});
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private addResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
let resultsToAdd: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToAdd = [resultSet];
|
||||
} else {
|
||||
resultsToAdd = resultSet;
|
||||
}
|
||||
|
||||
let tables: GridTable<any>[] = [];
|
||||
|
||||
for (let set of resultsToAdd) {
|
||||
let tableState = new GridTableState();
|
||||
let table = new GridTable(this.runner, tableState, set, this.contextMenuService);
|
||||
tableState.onMaximizedChange(e => {
|
||||
if (e) {
|
||||
this.maximizeTable(table.id);
|
||||
} else {
|
||||
this.minimizeTables();
|
||||
}
|
||||
});
|
||||
this.tableDisposable.push(attachTableStyler(table, this.themeService));
|
||||
|
||||
tables.push(table);
|
||||
}
|
||||
|
||||
if (isUndefinedOrNull(this.maximizedGrid)) {
|
||||
this.splitView.addViews(tables, tables.map(i => i.minimumSize), this.splitView.length);
|
||||
}
|
||||
|
||||
this.tables = this.tables.concat(tables);
|
||||
}
|
||||
|
||||
private reset() {
|
||||
for (let i = this.splitView.length - 1; i >= 0; i--) {
|
||||
this.splitView.removeView(i);
|
||||
}
|
||||
dispose(this.tables);
|
||||
this.tables = [];
|
||||
}
|
||||
|
||||
private maximizeTable(tableid: string): void {
|
||||
if (!this.tables.find(t => t.id === tableid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = this.tables.length - 1; i >= 0; i--) {
|
||||
if (this.tables[i].id === tableid) {
|
||||
this.tables[i].state.maximized = true;
|
||||
this.maximizedGrid = this.tables[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
this.splitView.removeView(i);
|
||||
}
|
||||
}
|
||||
|
||||
private minimizeTables(): void {
|
||||
if (this.maximizedGrid) {
|
||||
this.maximizedGrid.state.maximized = false;
|
||||
this.maximizedGrid = undefined;
|
||||
this.splitView.removeView(0);
|
||||
this.splitView.addViews(this.tables, this.tables.map(i => i.minimumSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GridTable<T> extends Disposable implements IView {
|
||||
private static BOTTOMPADDING = 5;
|
||||
private static ACTIONBAR_WIDTH = 26;
|
||||
private table: Table<T>;
|
||||
private actionBar: ActionBar;
|
||||
private container = document.createElement('div');
|
||||
private selectionModel = new CellSelectionModel();
|
||||
private styles: ITableStyles;
|
||||
|
||||
private columns: Slick.Column<T>[];
|
||||
|
||||
private _onDidChange = new Emitter<number>();
|
||||
public readonly onDidChange: Event<number> = this._onDidChange.event;
|
||||
|
||||
public id = generateUuid();
|
||||
|
||||
constructor(
|
||||
private runner: QueryRunner,
|
||||
public state: GridTableState,
|
||||
private resultSet: sqlops.ResultSetSummary,
|
||||
private contextMenuService: IContextMenuService
|
||||
) {
|
||||
super();
|
||||
this.container.style.width = '100%';
|
||||
this.container.style.height = '100%';
|
||||
this.container.style.marginBottom = GridTable.BOTTOMPADDING + 'px';
|
||||
this.container.className = 'grid-panel';
|
||||
|
||||
this.columns = this.resultSet.columnInfo.map((c, i) => {
|
||||
return <Slick.Column<T>>{
|
||||
id: i.toString(),
|
||||
name: c.columnName,
|
||||
field: i.toString(),
|
||||
width: 100
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public render(container: HTMLElement, orientation: Orientation): void {
|
||||
container.appendChild(this.container);
|
||||
}
|
||||
|
||||
private build(): void {
|
||||
let tableContainer = document.createElement('div');
|
||||
tableContainer.style.display = 'inline-block';
|
||||
|
||||
this.container.appendChild(tableContainer);
|
||||
|
||||
let collection = new VirtualizedCollection(50, this.resultSet.rowCount,
|
||||
(offset, count) => this.loadData(offset, count),
|
||||
index => this.placeholdGenerator(index)
|
||||
);
|
||||
collection.setCollectionChangedCallback((change, startIndex, count) => {
|
||||
this.renderGridDataRowsRange(startIndex, count);
|
||||
});
|
||||
let numberColumn = new RowNumberColumn({ numberOfRows: this.resultSet.rowCount });
|
||||
this.columns.unshift(numberColumn.getColumnDefinition());
|
||||
this.table = this._register(new Table(tableContainer, { dataProvider: new AsyncDataProvider(collection), columns: this.columns }, { rowHeight, showRowNumber: true }));
|
||||
this.table.setSelectionModel(this.selectionModel);
|
||||
this.table.registerPlugin(new MouseWheelSupport());
|
||||
this.table.registerPlugin(new AutoColumnSize());
|
||||
this.table.registerPlugin(numberColumn);
|
||||
this._register(this.table.onContextMenu(this.contextMenu, this));
|
||||
|
||||
if (this.styles) {
|
||||
this.table.style(this.styles);
|
||||
}
|
||||
|
||||
let actions = [];
|
||||
|
||||
if (this.state.canBeMaximized) {
|
||||
if (this.state.maximized) {
|
||||
actions.splice(1, 0, new MinimizeTableAction());
|
||||
} else {
|
||||
actions.splice(1, 0, new MaximizeTableAction());
|
||||
}
|
||||
}
|
||||
|
||||
actions.push(
|
||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON)
|
||||
);
|
||||
|
||||
let actionBarContainer = document.createElement('div');
|
||||
actionBarContainer.style.width = GridTable.ACTIONBAR_WIDTH + 'px';
|
||||
actionBarContainer.style.display = 'inline-block';
|
||||
actionBarContainer.style.height = '100%';
|
||||
actionBarContainer.style.verticalAlign = 'top';
|
||||
this.container.appendChild(actionBarContainer);
|
||||
this.actionBar = new ActionBar(actionBarContainer, {
|
||||
orientation: ActionsOrientation.VERTICAL, context: {
|
||||
runner: this.runner,
|
||||
batchId: this.resultSet.batchId,
|
||||
resultId: this.resultSet.id,
|
||||
table: this.table,
|
||||
tableState: this.state
|
||||
}
|
||||
});
|
||||
this.actionBar.push(actions, { icon: true, label: false });
|
||||
}
|
||||
|
||||
public layout(size: number): void {
|
||||
if (!this.table) {
|
||||
this.build();
|
||||
}
|
||||
this.table.layout(
|
||||
new Dimension(
|
||||
getContentWidth(this.container) - GridTable.ACTIONBAR_WIDTH,
|
||||
size - GridTable.BOTTOMPADDING
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public get minimumSize(): number {
|
||||
let smallestRows = ((this.resultSet.rowCount) * rowHeight) + columnHeight + estimatedScrollBarHeight + GridTable.BOTTOMPADDING;
|
||||
let smallestSize = (minGridHeightInRows * rowHeight) + columnHeight + estimatedScrollBarHeight + GridTable.BOTTOMPADDING;
|
||||
return Math.min(smallestRows, smallestSize);
|
||||
}
|
||||
|
||||
public get maximumSize(): number {
|
||||
return ((this.resultSet.rowCount) * rowHeight) + columnHeight + estimatedScrollBarHeight + GridTable.BOTTOMPADDING;
|
||||
}
|
||||
|
||||
private loadData(offset: number, count: number): Thenable<T[]> {
|
||||
return this.runner.getQueryRows(offset, count, this.resultSet.batchId, this.resultSet.id).then(response => {
|
||||
return response.resultSubset.rows.map(r => {
|
||||
let dataWithSchema = {};
|
||||
// skip the first column since its a number column
|
||||
for (let i = 1; i < this.columns.length; i++) {
|
||||
dataWithSchema[this.columns[i].field] = r[i - 1].displayValue;
|
||||
}
|
||||
return dataWithSchema as T;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private contextMenu(e: ITableContextMenuEvent): void {
|
||||
const selection = this.selectionModel.getSelectedRanges();
|
||||
const { cell } = e;
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => {
|
||||
let actions = [
|
||||
new SelectAllGridAction(),
|
||||
new Separator(),
|
||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||
new Separator(),
|
||||
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false),
|
||||
new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true)
|
||||
];
|
||||
|
||||
if (this.state.canBeMaximized) {
|
||||
if (this.state.maximized) {
|
||||
actions.splice(1, 0, new MinimizeTableAction());
|
||||
} else {
|
||||
actions.splice(1, 0, new MaximizeTableAction());
|
||||
}
|
||||
}
|
||||
|
||||
return TPromise.as(actions);
|
||||
},
|
||||
getActionsContext: () => {
|
||||
return <IGridActionContext>{
|
||||
cell,
|
||||
selection,
|
||||
runner: this.runner,
|
||||
batchId: this.resultSet.batchId,
|
||||
resultId: this.resultSet.id,
|
||||
table: this.table,
|
||||
tableState: this.state
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private placeholdGenerator(index: number): any {
|
||||
return { values: [] };
|
||||
}
|
||||
|
||||
private renderGridDataRowsRange(startIndex: number, count: number): void {
|
||||
// let editor = this.table.getCellEditor();
|
||||
// let oldValue = editor ? editor.getValue() : undefined;
|
||||
// let wasValueChanged = editor ? editor.isValueChanged() : false;
|
||||
this.invalidateRange(startIndex, startIndex + count);
|
||||
// let activeCell = this._grid.getActiveCell();
|
||||
// if (editor && activeCell.row >= startIndex && activeCell.row < startIndex + count) {
|
||||
// if (oldValue && wasValueChanged) {
|
||||
// editor.setValue(oldValue);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private invalidateRange(start: number, end: number): void {
|
||||
let refreshedRows = range(start, end);
|
||||
if (this.table) {
|
||||
this.table.invalidateRows(refreshedRows, true);
|
||||
}
|
||||
}
|
||||
|
||||
public style(styles: ITableStyles) {
|
||||
if (this.table) {
|
||||
this.table.style(styles);
|
||||
} else {
|
||||
this.styles = styles;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
$(this.container).destroy();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
32
src/sql/parts/query/editor/media/messagePanel.css
Normal file
32
src/sql/parts/query/editor/media/messagePanel.css
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
/* Disable repl hover highlight in tree. */
|
||||
.monaco-workbench .message-tree .monaco-tree .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
/* Disable repl hover highlight in tree. */
|
||||
.monaco-workbench .message-tree .monaco-tree .monaco-tree-row > .content {
|
||||
line-height: 18px;
|
||||
user-select: text;
|
||||
word-wrap: break-word;
|
||||
/* white-space: pre-wrap; */
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.message-tree .time-stamp {
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message-tree .message,
|
||||
.message-tree .batch-start {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message-tree .batch-start {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-tree .batch-start:hover {
|
||||
color: red;
|
||||
}
|
||||
343
src/sql/parts/query/editor/messagePanel.ts
Normal file
343
src/sql/parts/query/editor/messagePanel.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./media/messagePanel';
|
||||
import { IMessagesActionContext, SelectAllMessagesAction, CopyMessagesAction } from './actions';
|
||||
import QueryRunner from 'sql/parts/query/execution/queryRunner';
|
||||
|
||||
import { IResultMessage, BatchSummary, ISelectionData } from 'sqlops';
|
||||
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { OpenMode, ClickBehavior, ICancelableEvent, IControllerOptions } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { WorkbenchTreeController } from 'vs/platform/list/browser/listService';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { $ } from 'vs/base/browser/builder';
|
||||
import { isArray } from 'vs/base/common/types';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditor } from 'vs/editor/common/editorCommon';
|
||||
|
||||
export interface IResultMessageIntern extends IResultMessage {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface IMessagePanelMessage {
|
||||
message: string;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export interface IMessagePanelBatchMessage extends IMessagePanelMessage {
|
||||
selection: ISelectionData;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface IMessageTemplate {
|
||||
message: HTMLElement;
|
||||
}
|
||||
|
||||
interface IBatchTemplate extends IMessageTemplate {
|
||||
timeStamp: HTMLElement;
|
||||
}
|
||||
|
||||
const TemplateIds = {
|
||||
MESSAGE: 'message',
|
||||
BATCH: 'batch',
|
||||
MODEL: 'model'
|
||||
};
|
||||
|
||||
export class MessagePanel extends ViewletPanel {
|
||||
private ds = new MessageDataSource();
|
||||
private renderer = new MessageRenderer();
|
||||
private model = new Model();
|
||||
private controller: MessageController;
|
||||
private container = $('div message-tree').getHTMLElement();
|
||||
|
||||
private queryRunnerDisposables: IDisposable[] = [];
|
||||
|
||||
private tree: ITree;
|
||||
|
||||
constructor(
|
||||
title: string, options: IViewletPanelOptions,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IInstantiationService instantiationService: IInstantiationService
|
||||
) {
|
||||
super(title, options, keybindingService, contextMenuService, configurationService);
|
||||
this.controller = instantiationService.createInstance(MessageController, { openMode: OpenMode.SINGLE_CLICK, clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change, to preserve focus behaviour in input field */ });
|
||||
this.controller.toFocusOnClick = this.model;
|
||||
this.tree = new Tree(this.container, {
|
||||
dataSource: this.ds,
|
||||
renderer: this.renderer,
|
||||
controller: this.controller
|
||||
}, { keyboardSupport: false });
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this.container.style.width = '100%';
|
||||
this.container.style.height = '100%';
|
||||
attachListStyler(this.tree, this.themeService);
|
||||
container.appendChild(this.container);
|
||||
this.tree.setInput(this.model);
|
||||
}
|
||||
|
||||
protected layoutBody(size: number): void {
|
||||
const previousScrollPosition = this.tree.getScrollPosition();
|
||||
this.tree.layout(size);
|
||||
if (previousScrollPosition === 1) {
|
||||
this.tree.setScrollPosition(1);
|
||||
}
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
dispose(this.queryRunnerDisposables);
|
||||
this.queryRunnerDisposables = [];
|
||||
this.reset();
|
||||
this.queryRunnerDisposables.push(runner.onQueryStart(() => this.reset()));
|
||||
this.queryRunnerDisposables.push(runner.onBatchStart(e => this.onBatchStart(e)));
|
||||
this.queryRunnerDisposables.push(runner.onMessage(e => this.onMessage(e)));
|
||||
this.queryRunnerDisposables.push(runner.onQueryEnd(e => this.onQueryEnd(e)));
|
||||
}
|
||||
|
||||
private onMessage(message: IResultMessage | IResultMessage[]) {
|
||||
if (isArray(message)) {
|
||||
this.model.messages.push(...message.map(c => {
|
||||
return <IMessagePanelMessage>{
|
||||
isError: c.isError,
|
||||
message: c.message
|
||||
};
|
||||
}));
|
||||
} else {
|
||||
this.model.messages.push({
|
||||
message: message.message,
|
||||
isError: message.isError
|
||||
});
|
||||
}
|
||||
const previousScrollPosition = this.tree.getScrollPosition();
|
||||
this.tree.refresh(this.model).then(() => {
|
||||
if (previousScrollPosition === 1) {
|
||||
this.tree.setScrollPosition(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onBatchStart(batch: BatchSummary) {
|
||||
this.model.messages.push({
|
||||
message: localize('query.message.startQuery', 'Started executing query at Line {0}', batch.selection.startLine),
|
||||
time: new Date(batch.executionStart).toLocaleTimeString(),
|
||||
selection: batch.selection,
|
||||
isError: false
|
||||
});
|
||||
const previousScrollPosition = this.tree.getScrollPosition();
|
||||
this.tree.refresh(this.model).then(() => {
|
||||
if (previousScrollPosition === 1) {
|
||||
this.tree.setScrollPosition(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onQueryEnd(elapsedTime: string) {
|
||||
this.model.totalExecuteMessage = {
|
||||
message: localize('query.message.executionTime', 'Total execution time: {0}', elapsedTime),
|
||||
isError: false
|
||||
};
|
||||
const previousScrollPosition = this.tree.getScrollPosition();
|
||||
this.tree.refresh(this.model).then(() => {
|
||||
if (previousScrollPosition === 1) {
|
||||
this.tree.setScrollPosition(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.model.messages = [];
|
||||
this.model.totalExecuteMessage = undefined;
|
||||
this.tree.refresh(this.model);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageDataSource implements IDataSource {
|
||||
getId(tree: ITree, element: Model | IResultMessageIntern): string {
|
||||
if (element instanceof Model) {
|
||||
return element.uuid;
|
||||
} else {
|
||||
if (!element.id) {
|
||||
element.id = generateUuid();
|
||||
}
|
||||
return element.id;
|
||||
}
|
||||
}
|
||||
|
||||
hasChildren(tree: ITree, element: any): boolean {
|
||||
return element instanceof Model;
|
||||
}
|
||||
|
||||
getChildren(tree: ITree, element: any): TPromise {
|
||||
if (element instanceof Model) {
|
||||
let messages = element.messages;
|
||||
if (element.totalExecuteMessage) {
|
||||
messages = messages.concat(element.totalExecuteMessage);
|
||||
}
|
||||
return TPromise.as(messages);
|
||||
} else {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
getParent(tree: ITree, element: any): TPromise {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageRenderer implements IRenderer {
|
||||
getHeight(tree: ITree, element: any): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(tree: ITree, element: any): string {
|
||||
if (element instanceof Model) {
|
||||
return TemplateIds.MODEL;
|
||||
} else if (element.selection) {
|
||||
return TemplateIds.BATCH;
|
||||
} else {
|
||||
return TemplateIds.MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IMessageTemplate | IBatchTemplate {
|
||||
|
||||
if (templateId === TemplateIds.MESSAGE) {
|
||||
$('div.time-stamp').appendTo(container);
|
||||
const message = $('div.message').appendTo(container).getHTMLElement();
|
||||
return { message };
|
||||
} else if (templateId === TemplateIds.BATCH) {
|
||||
const timeStamp = $('div.time-stamp').appendTo(container).getHTMLElement();
|
||||
const message = $('div.batch-start').appendTo(container).getHTMLElement();
|
||||
return { message, timeStamp };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
renderElement(tree: ITree, element: IResultMessage, templateId: string, templateData: IMessageTemplate | IBatchTemplate): void {
|
||||
if (templateId === TemplateIds.MESSAGE) {
|
||||
let data: IMessageTemplate = templateData;
|
||||
data.message.innerText = element.message;
|
||||
} else if (templateId === TemplateIds.BATCH) {
|
||||
let data = templateData as IBatchTemplate;
|
||||
data.timeStamp.innerText = element.time;
|
||||
data.message.innerText = element.message;
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageController extends WorkbenchTreeController {
|
||||
|
||||
private lastSelectedString: string = null;
|
||||
public toFocusOnClick: { focus(): void };
|
||||
|
||||
constructor(
|
||||
options: IControllerOptions,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IWorkbenchEditorService private workbenchEditorService: IWorkbenchEditorService,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
super(options, configurationService);
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
|
||||
const mouseEvent = <IMouseEvent>eventish;
|
||||
// input and output are one element in the tree => we only expand if the user clicked on the output.
|
||||
// if ((element.reference > 0 || (element instanceof RawObjectReplElement && element.hasChildren)) && mouseEvent.target.className.indexOf('input expression') === -1) {
|
||||
super.onLeftClick(tree, element, eventish, origin);
|
||||
tree.clearFocus();
|
||||
tree.deselect(element);
|
||||
// }
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection.type !== 'Range' || this.lastSelectedString === selection.toString()) {
|
||||
// only focus the input if the user is not currently selecting.
|
||||
this.toFocusOnClick.focus();
|
||||
}
|
||||
this.lastSelectedString = selection.toString();
|
||||
|
||||
if (element.selection) {
|
||||
let selection: ISelectionData = element.selection;
|
||||
// this is a batch statement
|
||||
let control = this.workbenchEditorService.getActiveEditor().getControl() as IEditor;
|
||||
control.setSelection({
|
||||
startColumn: selection.startColumn + 1,
|
||||
endColumn: selection.endColumn + 1,
|
||||
endLineNumber: selection.endLine + 1,
|
||||
startLineNumber: selection.startLine + 1
|
||||
});
|
||||
control.focus();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean {
|
||||
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return false; // allow context menu on input fields
|
||||
}
|
||||
|
||||
// Prevent native context menu from showing up
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const selection = document.getSelection();
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => {
|
||||
return { x: event.posx, y: event.posy };
|
||||
},
|
||||
getActions: () => {
|
||||
return TPromise.as([
|
||||
this.instantiationService.createInstance(CopyMessagesAction),
|
||||
new SelectAllMessagesAction()
|
||||
]);
|
||||
},
|
||||
getActionsContext: () => {
|
||||
return <IMessagesActionContext>{
|
||||
selection,
|
||||
tree
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Model {
|
||||
public messages: Array<IMessagePanelMessage | IMessagePanelBatchMessage> = [];
|
||||
public totalExecuteMessage: IMessagePanelMessage;
|
||||
|
||||
public uuid = generateUuid();
|
||||
|
||||
public focus() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
@@ -14,10 +13,10 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||
import { Configuration } from 'vs/editor/browser/config/configuration';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
|
||||
@@ -25,8 +24,7 @@ import { bootstrapAngular } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { IQueryComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { QueryOutputModule } from 'sql/parts/query/views/queryOutput.module';
|
||||
import { QUERY_OUTPUT_SELECTOR } from 'sql/parts/query/views/queryOutput.component';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { QueryResultsView } from 'sql/parts/query/editor/queryResultsView';
|
||||
|
||||
export const RESULTS_GRID_DEFAULTS = {
|
||||
cellPadding: [6, 10, 5],
|
||||
@@ -97,6 +95,8 @@ export class QueryResultsEditor extends BaseEditor {
|
||||
protected _rawOptions: BareResultsGridInfo;
|
||||
protected _input: QueryResultsInput;
|
||||
|
||||
private resultsView: QueryResultsView;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@@ -106,12 +106,12 @@ export class QueryResultsEditor extends BaseEditor {
|
||||
) {
|
||||
super(QueryResultsEditor.ID, telemetryService, themeService);
|
||||
this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('resultsGrid')) {
|
||||
this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
this.applySettings();
|
||||
}
|
||||
});
|
||||
// this._configurationService.onDidChangeConfiguration(e => {
|
||||
// if (e.affectsConfiguration('resultsGrid')) {
|
||||
// this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
// this.applySettings();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
public get input(): QueryResultsInput {
|
||||
@@ -136,54 +136,22 @@ export class QueryResultsEditor extends BaseEditor {
|
||||
}
|
||||
|
||||
createEditor(parent: HTMLElement): void {
|
||||
if (!this.resultsView) {
|
||||
this.resultsView = new QueryResultsView(parent, this._instantiationService, this._queryModelService);
|
||||
}
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
this.resultsView.layout(dimension);
|
||||
}
|
||||
|
||||
setInput(input: QueryResultsInput, options: EditorOptions): TPromise<void> {
|
||||
super.setInput(input, options);
|
||||
this.applySettings();
|
||||
if (!input.hasBootstrapped) {
|
||||
this._bootstrapAngular();
|
||||
}
|
||||
this.resultsView.input = input;
|
||||
return TPromise.wrap<void>(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private _bootstrapAngular(): void {
|
||||
let input = <QueryResultsInput>this.input;
|
||||
let uri = input.uri;
|
||||
|
||||
// Pass the correct DataService to the new angular component
|
||||
let dataService = this._queryModelService.getDataService(uri);
|
||||
if (!dataService) {
|
||||
throw new Error('DataService not found for URI: ' + uri);
|
||||
}
|
||||
|
||||
// Mark that we have bootstrapped
|
||||
input.setBootstrappedTrue();
|
||||
|
||||
// Get the bootstrap params and perform the bootstrap
|
||||
// Note: pass in input so on disposal this is cleaned up.
|
||||
// Otherwise many components will be left around and be subscribed
|
||||
// to events from the backing data service
|
||||
let params: IQueryComponentParams = {
|
||||
dataService: dataService,
|
||||
onSaveViewState: this.input.onSaveViewStateEmitter.event,
|
||||
onRestoreViewState: this.input.onRestoreViewStateEmitter.event
|
||||
};
|
||||
bootstrapAngular(this._instantiationService,
|
||||
QueryOutputModule,
|
||||
this.getContainer(),
|
||||
QUERY_OUTPUT_SELECTOR,
|
||||
params,
|
||||
input);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
122
src/sql/parts/query/editor/queryResultsView.ts
Normal file
122
src/sql/parts/query/editor/queryResultsView.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { TabbedPanel, IPanelTab, IPanelView } from 'sql/base/browser/ui/panel/panel';
|
||||
import { IQueryModelService } from '../execution/queryModel';
|
||||
import QueryRunner from 'sql/parts/query/execution/queryRunner';
|
||||
import { MessagePanel } from './messagePanel';
|
||||
import { GridPanel } from './gridPanel';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as UUID from 'vs/base/common/uuid';
|
||||
import { PanelViewlet } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
class ResultsView implements IPanelView {
|
||||
private panelViewlet: PanelViewlet;
|
||||
private gridPanel: GridPanel;
|
||||
private messagePanel: MessagePanel;
|
||||
private container = document.createElement('div');
|
||||
|
||||
private _onRemove = new Emitter<void>();
|
||||
public readonly onRemove = this._onRemove.event;
|
||||
|
||||
private _onLayout = new Emitter<void>();
|
||||
public readonly onLayout = this._onLayout.event;
|
||||
|
||||
private queryRunnerDisposable: IDisposable[] = [];
|
||||
|
||||
constructor(instantiationService: IInstantiationService) {
|
||||
this.panelViewlet = instantiationService.createInstance(PanelViewlet, 'resultsView', { showHeaderInTitleWhenSingleView: false });
|
||||
this.gridPanel = instantiationService.createInstance(GridPanel, nls.localize('gridPanel', 'Results'), {});
|
||||
this.messagePanel = instantiationService.createInstance(MessagePanel, nls.localize('messagePanel', 'Messages'), {});
|
||||
this.panelViewlet.create(this.container).then(() => {
|
||||
this.panelViewlet.addPanels([
|
||||
{ panel: this.gridPanel, size: 1000, index: 0 },
|
||||
{ panel: this.messagePanel, size: this.messagePanel.minimumSize, index: 1 }
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
container.appendChild(this.container);
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
this.panelViewlet.layout(dimension);
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this.gridPanel.queryRunner = runner;
|
||||
this.messagePanel.queryRunner = runner;
|
||||
}
|
||||
}
|
||||
|
||||
class ResultsTab implements IPanelTab {
|
||||
public readonly title = nls.localize('resultsTabTitle', 'Results');
|
||||
public readonly identifier = UUID.generateUuid();
|
||||
public readonly view: ResultsView;
|
||||
|
||||
private _isAttached = false;
|
||||
|
||||
constructor(instantiationService: IInstantiationService) {
|
||||
this.view = new ResultsView(instantiationService);
|
||||
|
||||
this.view.onLayout(() => this._isAttached = true, this);
|
||||
this.view.onRemove(() => this._isAttached = false, this);
|
||||
}
|
||||
|
||||
public isAttached(): boolean {
|
||||
return this._isAttached;
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this.view.queryRunner = runner;
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryResultsView {
|
||||
private _panelView: TabbedPanel;
|
||||
private _input: QueryResultsInput;
|
||||
private resultsTab: ResultsTab;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IQueryModelService private queryModelService: IQueryModelService
|
||||
) {
|
||||
this.resultsTab = new ResultsTab(instantiationService);
|
||||
this._panelView = new TabbedPanel(container, { showHeaderWhenSingleView: false });
|
||||
}
|
||||
|
||||
public style() {
|
||||
|
||||
}
|
||||
|
||||
public set input(input: QueryResultsInput) {
|
||||
this._input = input;
|
||||
this.resultsTab.queryRunner = this.queryModelService._getQueryInfo(input.uri).queryRunner;
|
||||
// if (!this.resultsTab.isAttached) {
|
||||
this._panelView.pushTab(this.resultsTab);
|
||||
// }
|
||||
}
|
||||
|
||||
public get input(): QueryResultsInput {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
public layout(dimension: DOM.Dimension) {
|
||||
this._panelView.layout(dimension);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import QueryRunner from 'sql/parts/query/execution/queryRunner';
|
||||
import { DataService } from 'sql/parts/grid/services/dataService';
|
||||
import { ISlickRange } from 'angular2-slickgrid';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
EditRevertCellResult,
|
||||
ExecutionPlanOptions
|
||||
} from 'sqlops';
|
||||
import { QueryInfo } from 'sql/parts/query/execution/queryModelService';
|
||||
|
||||
export const SERVICE_ID = 'queryModelService';
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface IQueryModelService {
|
||||
resizeResultsets(uri: string): void;
|
||||
onAngularLoaded(uri: string): void;
|
||||
|
||||
copyResults(uri: string, selection: ISlickRange[], batchId: number, resultId: number, includeHeaders?: boolean): void;
|
||||
copyResults(uri: string, selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void;
|
||||
setEditorSelection(uri: string, index: number): void;
|
||||
showWarning(uri: string, message: string): void;
|
||||
showError(uri: string, message: string): void;
|
||||
@@ -69,6 +69,7 @@ export interface IQueryModelService {
|
||||
revertRow(ownerUri: string, rowId: number): Thenable<void>;
|
||||
getEditRows(ownerUri: string, rowStart: number, numberOfRows: number): Thenable<EditSubsetResult>;
|
||||
|
||||
_getQueryInfo(uri: string): QueryInfo;
|
||||
// Edit Data Callbacks
|
||||
onEditSessionReady: Event<EditSessionReadyParams>;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { SqlFlavorStatusbarItem } from 'sql/parts/query/common/flavorStatus';
|
||||
import { RowCountStatusBarItem } from 'sql/parts/query/common/rowCountStatus';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import { ISlickRange } from 'angular2-slickgrid';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as statusbar from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
@@ -31,7 +30,7 @@ import Severity from 'vs/base/common/severity';
|
||||
|
||||
const selectionSnippetMaxLen = 100;
|
||||
|
||||
interface QueryEvent {
|
||||
export interface QueryEvent {
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
||||
@@ -39,7 +38,7 @@ interface QueryEvent {
|
||||
/**
|
||||
* Holds information about the state of a query runner
|
||||
*/
|
||||
class QueryInfo {
|
||||
export class QueryInfo {
|
||||
public queryRunner: QueryRunner;
|
||||
public dataService: DataService;
|
||||
public queryEventQueue: QueryEvent[];
|
||||
@@ -170,7 +169,7 @@ export class QueryModelService implements IQueryModelService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public copyResults(uri: string, selection: ISlickRange[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||
public copyResults(uri: string, selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||
this._queryInfoMap.get(uri).queryRunner.copyResults(selection, batchId, resultId, includeHeaders);
|
||||
}
|
||||
|
||||
@@ -569,7 +568,7 @@ export class QueryModelService implements IQueryModelService {
|
||||
}
|
||||
}
|
||||
|
||||
private _getQueryInfo(uri: string): QueryInfo {
|
||||
public _getQueryInfo(uri: string): QueryInfo {
|
||||
return this._queryInfoMap.get(uri);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import * as sqlops from 'sqlops';
|
||||
import * as Constants from 'sql/parts/query/common/constants';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import { IQueryManagementService } from 'sql/parts/query/common/queryManagement';
|
||||
import { ISlickRange } from 'angular2-slickgrid';
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
import { SaveFormat } from 'sql/parts/grid/common/interfaces';
|
||||
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
@@ -21,6 +21,9 @@ import * as types from 'vs/base/common/types';
|
||||
import { EventEmitter } from 'sql/base/common/eventEmitter';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { Emitter, echo, debounceEvent, Event } from 'vs/base/common/event';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ResultSerializer } from 'sql/parts/query/common/resultSerializer';
|
||||
|
||||
export interface IEditSessionReadyEvent {
|
||||
ownerUri: string;
|
||||
@@ -61,6 +64,38 @@ export default class QueryRunner {
|
||||
private _batchSets: sqlops.BatchSummary[] = [];
|
||||
private _eventEmitter = new EventEmitter();
|
||||
|
||||
private _onMessage = new Emitter<sqlops.IResultMessage>();
|
||||
public readonly onMessage = debounceEvent<sqlops.IResultMessage, sqlops.IResultMessage[]>(echo(this._onMessage.event), (l, e) => {
|
||||
// on first run
|
||||
if (types.isUndefinedOrNull(l)) {
|
||||
return [e];
|
||||
} else {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
|
||||
private _onResultSet = new Emitter<sqlops.ResultSetSummary>();
|
||||
public readonly onResultSet = debounceEvent<sqlops.ResultSetSummary, sqlops.ResultSetSummary[]>(echo(this._onResultSet.event), (l, e) => {
|
||||
// on first run
|
||||
if (types.isUndefinedOrNull(l)) {
|
||||
return [e];
|
||||
} else {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
|
||||
private _onQueryStart = new Emitter<void>();
|
||||
public readonly onQueryStart: Event<void> = echo(this._onQueryStart.event);
|
||||
|
||||
private _onQueryEnd = new Emitter<string>();
|
||||
public readonly onQueryEnd: Event<string> = echo(this._onQueryEnd.event);
|
||||
|
||||
private _onBatchStart = new Emitter<sqlops.BatchSummary>();
|
||||
public readonly onBatchStart: Event<sqlops.BatchSummary> = echo(this._onBatchStart.event);
|
||||
|
||||
private _onBatchEnd = new Emitter<sqlops.BatchSummary>();
|
||||
public readonly onBatchEnd: Event<sqlops.BatchSummary> = echo(this._onBatchEnd.event);
|
||||
|
||||
// CONSTRUCTOR /////////////////////////////////////////////////////////
|
||||
constructor(
|
||||
public uri: string,
|
||||
@@ -68,7 +103,8 @@ export default class QueryRunner {
|
||||
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IWorkspaceConfigurationService private _workspaceConfigurationService: IWorkspaceConfigurationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) { }
|
||||
|
||||
get isExecuting(): boolean {
|
||||
@@ -152,6 +188,7 @@ export default class QueryRunner {
|
||||
|
||||
private handleSuccessRunQueryResult() {
|
||||
// The query has started, so lets fire up the result pane
|
||||
this._onQueryStart.fire();
|
||||
this._eventEmitter.emit(EventType.START);
|
||||
this._queryManagementService.registerRunner(this, this.uri);
|
||||
}
|
||||
@@ -187,8 +224,9 @@ export default class QueryRunner {
|
||||
}
|
||||
});
|
||||
|
||||
// We're done with this query so shut down any waiting mechanisms
|
||||
this._eventEmitter.emit(EventType.COMPLETE, Utils.parseNumAsTimeString(this._totalElapsedMilliseconds));
|
||||
// We're done with this query so shut down any waiting mechanisms
|
||||
this._onQueryEnd.fire(Utils.parseNumAsTimeString(this._totalElapsedMilliseconds));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,6 +247,7 @@ export default class QueryRunner {
|
||||
// Store the batch
|
||||
this.batchSets[batch.id] = batch;
|
||||
this._eventEmitter.emit(EventType.BATCH_START, batch);
|
||||
this._onBatchStart.fire(batch);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,7 +264,9 @@ export default class QueryRunner {
|
||||
// send a time message in the format used for query complete
|
||||
this.sendBatchTimeMessage(batch.id, Utils.parseNumAsTimeString(executionTime));
|
||||
}
|
||||
|
||||
this._eventEmitter.emit(EventType.BATCH_COMPLETE, batch);
|
||||
this._onBatchEnd.fire(batch);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,6 +297,7 @@ export default class QueryRunner {
|
||||
// Store the result set in the batch and emit that a result set has completed
|
||||
batchSet.resultSetSummaries[resultSet.id] = resultSet;
|
||||
this._eventEmitter.emit(EventType.RESULT_SET, resultSet);
|
||||
this._onResultSet.fire(resultSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,13 +311,13 @@ export default class QueryRunner {
|
||||
|
||||
// Send the message to the results pane
|
||||
this._eventEmitter.emit(EventType.MESSAGE, message);
|
||||
this._onMessage.fire(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get more data rows from the current resultSets from the service layer
|
||||
*/
|
||||
public getQueryRows(rowStart: number, numberOfRows: number, batchIndex: number, resultSetIndex: number): Thenable<sqlops.QueryExecuteSubsetResult> {
|
||||
const self = this;
|
||||
let rowData: sqlops.QueryExecuteSubsetParams = <sqlops.QueryExecuteSubsetParams>{
|
||||
ownerUri: this.uri,
|
||||
resultSetIndex: resultSetIndex,
|
||||
@@ -284,16 +326,12 @@ export default class QueryRunner {
|
||||
batchIndex: batchIndex
|
||||
};
|
||||
|
||||
return new Promise<sqlops.QueryExecuteSubsetResult>((resolve, reject) => {
|
||||
self._queryManagementService.getQueryRows(rowData).then(result => {
|
||||
resolve(result);
|
||||
}, error => {
|
||||
self._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
|
||||
});
|
||||
reject(error);
|
||||
return this._queryManagementService.getQueryRows(rowData).then(r => r, error => {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
|
||||
});
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,7 +355,7 @@ export default class QueryRunner {
|
||||
this._isExecuting = false;
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('query.initEditExecutionFailed', 'Init Edit Execution failed: ') + error
|
||||
message: nls.localize('query.initEditExecutionFailed', 'Init Edit Execution failed: ') + error
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -341,7 +379,7 @@ export default class QueryRunner {
|
||||
let error = `Nothing returned from subset query`;
|
||||
self._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: error
|
||||
message: error
|
||||
});
|
||||
reject(error);
|
||||
}
|
||||
@@ -350,7 +388,7 @@ export default class QueryRunner {
|
||||
let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
|
||||
self._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `${errorMessage} ${error}`
|
||||
message: `${errorMessage} ${error}`
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
@@ -412,7 +450,7 @@ export default class QueryRunner {
|
||||
* @param resultId The result id of the result to copy from
|
||||
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
|
||||
*/
|
||||
copyResults(selection: ISlickRange[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||
copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||
const self = this;
|
||||
let copyString = '';
|
||||
const eol = this.getEolString();
|
||||
@@ -483,7 +521,7 @@ export default class QueryRunner {
|
||||
return !!removeNewLines;
|
||||
}
|
||||
|
||||
private getColumnHeaders(batchId: number, resultId: number, range: ISlickRange): string[] {
|
||||
private getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
|
||||
let headers: string[] = undefined;
|
||||
let batchSummary: sqlops.BatchSummary = this.batchSets[batchId];
|
||||
if (batchSummary !== undefined) {
|
||||
@@ -519,7 +557,11 @@ export default class QueryRunner {
|
||||
isError: false
|
||||
};
|
||||
// Send the message to the results pane
|
||||
this._eventEmitter.emit(EventType.MESSAGE, message);
|
||||
this._onMessage.fire(message);
|
||||
}
|
||||
}
|
||||
|
||||
public serializeResults(batchId: number, resultSetId: number, format: SaveFormat, selection: Slick.Range[]) {
|
||||
return this.instantiationService.createInstance(ResultSerializer).saveResults(this.uri, { selection, format, batchIndex: batchId, resultSetNumber: resultSetId });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user