move code from parts to contrib (#8319)

This commit is contained in:
Anthony Dresser
2019-11-14 12:23:11 -08:00
committed by GitHub
parent 6438967202
commit 7a2c30e159
619 changed files with 848 additions and 848 deletions

View File

@@ -0,0 +1,33 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<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]="plugins[i]"
(onActiveCellChanged)="onActiveCellChanged($event)"
(onCellChange)="onCellEditEnd($event)"
(onContextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)"
(onRendered)="onGridRendered($event)"
[isCellEditValid]="onIsCellEditValid"
[overrideCellFn]="overrideCellFn"
[onBeforeAppendCell]="onBeforeAppendCell"
[enableEditing]="true"
class="boxCol content vertBox slickgrid">
</slick-grid>
</div>
</div>
</div>

View File

@@ -0,0 +1,726 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/editData';
import { ElementRef, ChangeDetectorRef, OnInit, OnDestroy, Component, Inject, forwardRef, EventEmitter } from '@angular/core';
import { VirtualizedCollection } from 'angular2-slickgrid';
import { IGridDataSet } from 'sql/workbench/contrib/grid/common/interfaces';
import * as Services from 'sql/base/browser/ui/table/formatters';
import { IEditDataComponentParams, IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import { GridParentComponent } from 'sql/workbench/contrib/editData/browser/gridParentComponent';
import { EditDataGridActionProvider } from 'sql/workbench/contrib/editData/browser/editDataGridActions';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin';
import { escape } from 'sql/base/common/strings';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { EditUpdateCellResult } from 'azdata';
import { ILogService } from 'vs/platform/log/common/log';
import { deepClone, assign } 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('./editData.component.html'))
})
export class EditDataComponent extends GridParentComponent implements OnInit, OnDestroy {
// The time(in milliseconds) we wait before refreshing the grid.
// We use clearTimeout and setTimeout pair to avoid unnecessary refreshes.
private refreshGridTimeoutInMs = 200;
// The timeout handle for the refresh grid task
private refreshGridTimeoutHandle: any;
// Optimized for the edit top 200 rows scenario, only need to retrieve the data once
// to make the scroll experience smoother
private windowSize = 200;
// FIELDS
// All datasets
private dataSet: IGridDataSet;
private firstRender = true;
// Current selected cell state
private currentCell: { row: number, column: number, isEditable: boolean, isDirty: boolean };
private currentEditCellValue: string;
private newRowVisible: boolean;
private removingNewRow: boolean;
private rowIdMappings: { [gridRowId: number]: number } = {};
private dirtyCells: number[] = [];
protected plugins = new Array<Array<Slick.Plugin<any>>>();
// Edit Data functions
public onActiveCellChanged: (event: Slick.OnActiveCellChangedEventArgs<any>) => void;
public onCellEditEnd: (event: Slick.OnCellChangeEventArgs<any>) => 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<{}[]>;
public onBeforeAppendCell: (row: number, column: number) => string;
public onGridRendered: (event: Slick.OnRenderedEventArgs<any>) => void;
private savedViewState: {
gridSelections: Slick.Range[];
scrollTop;
scrollLeft;
};
constructor(
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
@Inject(IBootstrapParams) params: IEditDataComponentParams,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(INotificationService) private notificationService: INotificationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IContextKeyService) contextKeyService: IContextKeyService,
@Inject(IConfigurationService) configurationService: IConfigurationService,
@Inject(IClipboardService) clipboardService: IClipboardService,
@Inject(IQueryEditorService) queryEditorService: IQueryEditorService,
@Inject(ILogService) logService: ILogService
) {
super(el, cd, contextMenuService, keybindingService, contextKeyService, configurationService, clipboardService, queryEditorService, logService);
this._el.nativeElement.className = 'slickgridContainer';
this.dataService = params.dataService;
this.actionProvider = this.instantiationService.createInstance(EditDataGridActionProvider, this.dataService, this.onGridSelectAll(), this.onDeleteRow(), this.onRevertRow());
params.onRestoreViewState(() => this.restoreViewState());
params.onSaveViewState(() => this.saveViewState());
}
/**
* 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:
this.logService.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;
this._cd.detectChanges();
// Hooking up edit functions
this.onIsCellEditValid = (row, column, value): boolean => {
// TODO can only run sync code
return true;
};
this.onActiveCellChanged = this.onCellSelect;
this.onCellEditEnd = (event: Slick.OnCellChangeEventArgs<any>): void => {
if (self.currentEditCellValue !== event.item[event.cell]) {
self.currentCell.isDirty = true;
}
// Store the value that was set
self.currentEditCellValue = event.item[event.cell];
};
this.overrideCellFn = (rowNumber, columnId, value?, data?): string => {
let returnVal = '';
// replace the line breaks with space since the edit text control cannot
// render line breaks and strips them, updating the value.
if (Services.DBCellValue.isDBCellValue(value)) {
returnVal = this.spacefyLinebreaks(value.displayValue);
} else if (typeof value === 'string') {
returnVal = this.spacefyLinebreaks(value);
}
return returnVal;
};
// This is the event slickgrid will raise in order to get the additional cell CSS classes for the cell
// Due to performance advantage we are using this event instead of the onViewportChanged event.
this.onBeforeAppendCell = (row: number, column: number): string => {
let cellClass = undefined;
if (this.isRowDirty(row) && column === 0) {
cellClass = ' dirtyRowHeader ';
} else if (this.isCellDirty(row, column)) {
cellClass = ' dirtyCell ';
}
return cellClass;
};
this.onGridRendered = (args: Slick.OnRenderedEventArgs<any>): void => {
// After rendering move the focus back to the previous active cell
if (this.currentCell.column !== undefined && this.currentCell.row !== undefined
&& this.isCellOnScreen(this.currentCell.row, this.currentCell.column)) {
this.focusCell(this.currentCell.row, this.currentCell.column, false);
}
};
// Setup a function for generating a promise to lookup result subsets
this.loadDataFunction = (offset: number, count: number): Promise<{}[]> => {
return self.dataService.getEditRows(offset, count).then(result => {
let gridData = result.subset.map(r => {
let dataWithSchema = {};
// skip the first column since its a number column
for (let i = 1; i < this.dataSet.columnDefinitions.length; i++) {
dataWithSchema[this.dataSet.columnDefinitions[i].field] = {
displayValue: r.cells[i - 1].displayValue,
ariaLabel: escape(r.cells[i - 1].displayValue),
isNull: r.cells[i - 1].isNull
};
}
return dataWithSchema;
});
// should add null row?
if (offset + count > this.dataSet.totalRows - 1) {
gridData.push(this.dataSet.columnDefinitions.reduce((p, c) => {
p[c.field] = 'NULL';
return p;
}, {}));
}
return gridData;
});
};
}
onDeleteRow(): (index: number) => void {
const self = this;
return (index: number): void => {
// If the user is deleting a new row that hasn't been committed yet then use the revert code
if (self.newRowVisible && index === self.dataSet.dataRows.getLength() - 2) {
self.revertCurrentRow();
}
else if (self.isNullRow(index)) {
// Don't try to delete NULL (new) row since it doesn't actually exist and will throw an error
// TODO #478 : We should really just stop the context menu from showing up for this row, but that's a bit more involved
// so until then at least make it not display an error
return;
}
else {
self.dataService.deleteRow(index)
.then(() => self.dataService.commitEdit())
.then(() => self.removeRow(index));
}
};
}
onRevertRow(): () => void {
const self = this;
return (): void => {
self.revertCurrentRow();
};
}
onCellSelect(event: Slick.OnActiveCellChangedEventArgs<any>): void {
let self = this;
let row = event.row;
let column = event.cell;
// Skip processing if the newly selected cell is undefined or we don't have column
// definition for the column (ie, the selection was reset)
if (row === undefined || column === undefined) {
return;
}
// Skip processing if the cell hasn't moved (eg, we reset focus to the previous cell after a failed update)
if (this.currentCell.row === row && this.currentCell.column === column && this.currentCell.isDirty === false) {
return;
}
let cellSelectTasks: Promise<void> = this.submitCurrentCellChange(
(result: EditUpdateCellResult) => {
// Cell update was successful, update the flags
self.setCellDirtyState(self.currentCell.row, self.currentCell.column, result.cell.isDirty);
self.setRowDirtyState(self.currentCell.row, result.isRowDirty);
return Promise.resolve();
},
(error) => {
// Cell update failed, jump back to the last cell we were on
self.focusCell(self.currentCell.row, self.currentCell.column, true);
return Promise.reject(null);
});
if (this.currentCell.row !== row) {
// We're changing row, commit the changes
cellSelectTasks = cellSelectTasks.then(() => {
return self.dataService.commitEdit().then(result => {
// Committing was successful, clean the grid
self.setGridClean();
self.rowIdMappings = {};
self.newRowVisible = false;
return Promise.resolve();
}, error => {
// Committing failed, jump back to the last selected cell
self.focusCell(self.currentCell.row, self.currentCell.column);
return Promise.reject(null);
});
});
}
// At the end of a successful cell select, update the currently selected cell
cellSelectTasks = cellSelectTasks.then(() => {
self.setCurrentCell(row, column);
});
// Cap off any failed promises, since they'll be handled
cellSelectTasks.catch(() => { });
}
handleComplete(self: EditDataComponent, event: any): void {
}
handleEditSessionReady(self, event): void {
// TODO: update when edit session is ready
}
handleMessage(self: EditDataComponent, event: any): void {
if (event.data && event.data.isError) {
self.notificationService.notify({
severity: Severity.Error,
message: event.data.message
});
}
}
handleResultSet(self: EditDataComponent, event: any): void {
// Clone the data before altering it to avoid impacting other subscribers
let resultSet = assign({}, event.data);
if (!resultSet.complete) {
return;
}
// 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);
let rowNumberColumn = new RowNumberColumn({ numberOfRows: resultSet.rowCount });
// Store the result set from the event
let dataSet: IGridDataSet = {
resized: undefined,
batchId: resultSet.batchId,
resultId: resultSet.id,
totalRows: resultSet.rowCount,
maxHeight: maxHeight,
minHeight: minHeight,
dataRows: new VirtualizedCollection(
self.windowSize,
resultSet.rowCount,
this.loadDataFunction,
index => { return {}; }
),
columnDefinitions: [rowNumberColumn.getColumnDefinition()].concat(resultSet.columnInfo.map((c, i) => {
let columnIndex = (i + 1).toString();
return {
id: columnIndex,
name: escape(c.columnName),
field: columnIndex,
formatter: Services.textFormatter,
isEditable: c.isUpdatable
};
}))
};
self.plugins.push([rowNumberColumn, new AutoColumnSize({ maxWidth: this.configurationService.getValue<number>('resultsGrid.maxColumnWidth') }), new AdditionalKeyBindings()]);
self.dataSet = dataSet;
// Create a dataSet to render without rows to reduce DOM size
let undefinedDataSet = deepClone(dataSet);
undefinedDataSet.columnDefinitions = dataSet.columnDefinitions;
undefinedDataSet.dataRows = undefined;
undefinedDataSet.resized = new EventEmitter();
self.placeHolderDataSets.push(undefinedDataSet);
self.refreshGrid();
// Setup the state of the selected cell
this.resetCurrentCell();
this.currentEditCellValue = undefined;
this.removingNewRow = false;
this.newRowVisible = false;
this.dirtyCells = [];
}
/**
* 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 {
this.refreshGrid();
}
/**
* Replace the line breaks with space.
*/
private spacefyLinebreaks(inputStr: string): string {
return inputStr.replace(/(\r\n|\n|\r)/g, ' ');
}
private refreshGrid(): Thenable<void> {
return new Promise<void>((resolve, reject) => {
const self = this;
clearTimeout(self.refreshGridTimeoutHandle);
this.refreshGridTimeoutHandle = setTimeout(() => {
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();
});
}
resolve();
}, self.refreshGridTimeoutInMs);
});
}
protected tryHandleKeyEvent(e: StandardKeyboardEvent): boolean {
let handled: boolean = false;
if (e.keyCode === KeyCode.Escape) {
this.revertCurrentRow();
handled = true;
}
return handled;
}
// Private Helper Functions ////////////////////////////////////////////////////////////////////////////
private async revertCurrentRow(): Promise<void> {
let currentNewRowIndex = this.dataSet.totalRows - 2;
if (this.newRowVisible && this.currentCell.row === currentNewRowIndex) {
// revert our last new row
this.removingNewRow = true;
this.dataService.revertRow(this.rowIdMappings[currentNewRowIndex])
.then(() => {
return this.removeRow(currentNewRowIndex);
}).then(() => {
this.newRowVisible = false;
this.resetCurrentCell();
});
} else {
try {
// Perform a revert row operation
if (this.currentCell && this.currentCell.row !== undefined) {
await this.dataService.revertRow(this.currentCell.row);
}
} finally {
// The operation may fail if there were no changes sent to the service to revert,
// so clear any existing client-side edit and refresh on-screen data
// do not refresh the whole dataset as it will move the focus away to the first row.
//
this.currentEditCellValue = undefined;
this.dirtyCells = [];
let row = this.currentCell.row;
this.resetCurrentCell();
if (row !== undefined) {
this.dataSet.dataRows.resetWindowsAroundIndex(row);
}
}
}
}
private submitCurrentCellChange(resultHandler, errorHandler): Promise<void> {
let self = this;
let updateCellPromise: Promise<void> = Promise.resolve();
let refreshGrid = false;
if (this.currentCell && this.currentCell.isEditable && this.currentEditCellValue !== undefined && !this.removingNewRow) {
if (this.isNullRow(this.currentCell.row)) {
refreshGrid = true;
// We've entered the "new row", so we need to add a row and jump to it
updateCellPromise = updateCellPromise.then(() => {
return self.addRow(this.currentCell.row);
});
}
// We're exiting a read/write cell after having changed the value, update the cell value in the service
updateCellPromise = updateCellPromise.then(() => {
// Use the mapped row ID if we're on that row
let sessionRowId = self.rowIdMappings[self.currentCell.row] !== undefined
? self.rowIdMappings[self.currentCell.row]
: self.currentCell.row;
return self.dataService.updateCell(sessionRowId, self.currentCell.column - 1, self.currentEditCellValue);
}).then(
result => {
self.currentEditCellValue = undefined;
let refreshPromise: Thenable<void> = Promise.resolve();
if (refreshGrid) {
refreshPromise = self.refreshGrid();
}
return refreshPromise.then(() => {
return resultHandler(result);
});
},
error => {
return errorHandler(error);
}
);
}
return updateCellPromise;
}
// 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
jQuery(grid.getCellNode(row, column)).addClass('dirtyCell').removeClass('selected');
if (this.dirtyCells.indexOf(column) === -1) {
this.dirtyCells.push(column);
}
} else {
jQuery(grid.getCellNode(row, column)).removeClass('dirtyCell');
if (this.dirtyCells.indexOf(column) !== -1) {
this.dirtyCells.splice(this.dirtyCells.indexOf(column), 1);
}
}
}
// 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
jQuery(grid.getCellNode(row, 0)).addClass('dirtyRowHeader');
} else {
jQuery(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 = jQuery(jQuery('.grid-canvas').children());
let allCells = jQuery(allRows.children());
allCells.removeClass('dirtyCell').removeClass('dirtyRowHeader');
this.dirtyCells = [];
}
// Adds an extra row to the end of slickgrid (just for rendering purposes)
// Then sets the focused call afterwards
private addRow(row: number): Thenable<void> {
let self = this;
// Add a new row to the edit session in the tools service
return this.dataService.createRow()
.then(result => {
// Map the new row ID to the row ID we have
self.rowIdMappings[row] = result.newRowId;
self.newRowVisible = true;
// Add a new "new row" to the end of the results
// Adding an extra row for 'new row' functionality
self.dataSet.totalRows++;
self.dataSet.maxHeight = self.getMaxHeight(self.dataSet.totalRows);
self.dataSet.minHeight = self.getMinHeight(self.dataSet.totalRows);
self.dataSet.dataRows = new VirtualizedCollection(
self.windowSize,
self.dataSet.totalRows,
self.loadDataFunction,
index => { return {}; }
);
});
}
// removes a row from the end of slickgrid (just for rendering purposes)
// Then sets the focused call afterwards
private removeRow(row: number): Thenable<void> {
// Removing the new row
this.dataSet.totalRows--;
this.dataSet.dataRows = new VirtualizedCollection(
this.windowSize,
this.dataSet.totalRows,
this.loadDataFunction,
index => { return {}; }
);
// refresh results view
return this.refreshGrid().then(() => {
// Set focus to the row index column of the removed row if the current selection is in the removed row
if (this.currentCell.row === row && !this.removingNewRow) {
this.focusCell(row, 1);
}
this.removingNewRow = false;
});
}
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);
}
private saveViewState(): void {
let grid = this.slickgrids.toArray()[0];
let self = this;
if (grid) {
let gridSelections = grid.getSelectedRanges();
let gridObject = grid as any;
let viewport = (gridObject._grid.getCanvasNode() as HTMLElement).parentElement;
this.savedViewState = {
gridSelections,
scrollTop: viewport.scrollTop,
scrollLeft: viewport.scrollLeft
};
// Save the cell that is currently being edited.
// Note: This is only updating the data in tools service, not saving the change to database.
// This is added to fix the data inconsistency: the updated value is displayed but won't be saved to the database
// when committing the changes for the row.
if (this.currentCell.row !== undefined && this.currentCell.column !== undefined && this.currentCell.isEditable) {
gridObject._grid.getEditorLock().commitCurrentEdit();
this.submitCurrentCellChange((result: EditUpdateCellResult) => {
self.setCellDirtyState(self.currentCell.row, self.currentCell.column, result.cell.isDirty);
}, (error: any) => {
self.notificationService.error(error);
});
}
}
}
private restoreViewState(): void {
if (this.savedViewState) {
this.slickgrids.toArray()[0].selection = this.savedViewState.gridSelections;
let viewport = ((this.slickgrids.toArray()[0] as any)._grid.getCanvasNode() as HTMLElement).parentElement;
viewport.scrollLeft = this.savedViewState.scrollLeft;
viewport.scrollTop = this.savedViewState.scrollTop;
this.savedViewState = undefined;
// This block of code is responsible for restoring the dirty state indicators if slickgrid decides not to re-render the dirty row
// Other scenarios will be taken care of by getAdditionalCssClassesForCell method when slickgrid needs to re-render the rows.
if (this.currentCell.row !== undefined) {
if (this.isRowDirty(this.currentCell.row)) {
this.setRowDirtyState(this.currentCell.row, true);
this.dirtyCells.forEach(cell => {
this.setCellDirtyState(this.currentCell.row, cell, true);
});
}
}
}
}
private isRowDirty(row: number): boolean {
return this.currentCell.row === row && this.dirtyCells.length > 0;
}
private isCellDirty(row: number, column: number): boolean {
return this.currentCell.row === row && this.dirtyCells.indexOf(column) !== -1;
}
private isCellOnScreen(row: number, column: number): boolean {
let slick: any = this.slickgrids.toArray()[0];
let grid = slick._grid;
let viewport = grid.getViewport();
let cellBox = grid.getCellNodeBox(row, column);
return viewport && cellBox
&& viewport.leftPx <= cellBox.left && viewport.rightPx >= cellBox.right
&& viewport.top <= row && viewport.bottom >= row;
}
private resetCurrentCell() {
this.currentCell = {
row: undefined,
column: undefined,
isEditable: false,
isDirty: false
};
}
private setCurrentCell(row: number, column: number) {
// Only update if we're actually changing cells
if (this.currentCell && (row !== this.currentCell.row || column !== this.currentCell.column)) {
this.currentCell = {
row: row,
column: column,
isEditable: this.dataSet.columnDefinitions[column]
? this.dataSet.columnDefinitions[column].isEditable
: false,
isDirty: false
};
}
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditDataEditor } from 'sql/workbench/contrib/editData/browser/editDataEditor';
import { EditDataInput } from 'sql/workbench/contrib/editData/browser/editDataInput';
import { EditDataResultsEditor } from 'sql/workbench/contrib/editData/browser/editDataResultsEditor';
import { EditDataResultsInput } from 'sql/workbench/contrib/editData/browser/editDataResultsInput';
import { EditorDescriptor, IEditorRegistry, Extensions } from 'vs/workbench/browser/editor';
import { Registry } from 'vs/platform/registry/common/platform';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
// Editor
const editDataEditorDescriptor = new EditorDescriptor(
EditDataEditor,
EditDataEditor.ID,
'EditData'
);
Registry.as<IEditorRegistry>(Extensions.Editors)
.registerEditor(editDataEditorDescriptor, [new SyncDescriptor(EditDataInput)]);
// Editor
const editDataResultsEditorDescriptor = new EditorDescriptor(
EditDataResultsEditor,
EditDataResultsEditor.ID,
'EditDataResults'
);
Registry.as<IEditorRegistry>(Extensions.Editors)
.registerEditor(editDataResultsEditorDescriptor, [new SyncDescriptor(EditDataResultsInput)]);

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { SlickGrid } from 'angular2-slickgrid';
import { EditDataComponent } from 'sql/workbench/contrib/editData/browser/editData.component';
import { providerIterator } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IBootstrapParams, ISelector } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
export const EditDataModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): Type<any> => {
@NgModule({
imports: [
CommonModule,
BrowserModule
],
declarations: [
EditDataComponent,
SlickGrid
],
entryComponents: [
EditDataComponent
],
providers: [
{ provide: IBootstrapParams, useValue: params },
{ provide: ISelector, useValue: selector },
...providerIterator(instantiationService)
]
})
class ModuleClass {
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
@Inject(ISelector) private selector: string
) {
}
ngDoBootstrap(appRef: ApplicationRef) {
const factory = this._resolver.resolveComponentFactory(EditDataComponent);
(<any>factory).factory.selector = this.selector;
appRef.bootstrap(factory);
}
}
return ModuleClass;
};

View File

@@ -0,0 +1,258 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action, IActionViewItem, IActionRunner } from 'vs/base/common/actions';
import { Disposable } from 'vs/base/common/lifecycle';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { EditDataEditor } from 'sql/workbench/contrib/editData/browser/editDataEditor';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { firstIndex } from 'vs/base/common/arrays';
const $ = dom.$;
/**
* Action class that edit data based actions will extend
*/
export abstract class EditDataAction extends Action {
private _classes: string[];
constructor(protected editor: EditDataEditor, id: string, enabledClass: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService) {
super(id);
this.enabled = true;
this.setClass(enabledClass);
}
/**
* This method is executed when the button is clicked.
*/
public abstract run(): Promise<void>;
protected setClass(enabledClass: string): void {
this._classes = [];
if (enabledClass) {
this._classes.push(enabledClass);
}
this.class = this._classes.join(' ');
}
/**
* Returns the URI of the given editor if it is not undefined and is connected.
*/
public isConnected(editor: EditDataEditor): boolean {
if (!editor || !editor.uri) {
return false;
}
return this._connectionManagementService.isConnected(editor.uri);
}
}
/**
* Action class that refreshes the table for an edit data session
*/
export class RefreshTableAction extends EditDataAction {
private static EnabledClass = 'start';
public static ID = 'refreshTableAction';
constructor(editor: EditDataEditor,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService,
@INotificationService private _notificationService: INotificationService,
) {
super(editor, RefreshTableAction.ID, RefreshTableAction.EnabledClass, _connectionManagementService);
this.label = nls.localize('editData.run', "Run");
}
public run(): Promise<void> {
if (this.isConnected(this.editor)) {
let input = this.editor.editDataInput;
let rowLimit: number = undefined;
let queryString: string = undefined;
if (input.queryPaneEnabled) {
queryString = input.queryString = this.editor.getEditorText();
} else {
rowLimit = input.rowLimit;
}
this._queryModelService.disposeEdit(input.uri).then((result) => {
this._queryModelService.initializeEdit(input.uri, input.schemaName, input.tableName, input.objectType, rowLimit, queryString);
input.showResultsEditor();
}, error => {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('disposeEditFailure', "Dispose Edit Failed With Error: ") + error
});
});
}
return Promise.resolve(null);
}
}
/**
* Action class that cancels the refresh data trigger in an edit data session
*/
export class StopRefreshTableAction extends EditDataAction {
private static EnabledClass = 'stop';
public static ID = 'stopRefreshAction';
constructor(editor: EditDataEditor,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService
) {
super(editor, StopRefreshTableAction.ID, StopRefreshTableAction.EnabledClass, _connectionManagementService);
this.enabled = false;
this.label = nls.localize('editData.stop', "Stop");
}
public run(): Promise<void> {
let input = this.editor.editDataInput;
this._queryModelService.disposeEdit(input.uri);
return Promise.resolve(null);
}
}
/**
* Action class that is tied with ChangeMaxRowsActionItem
*/
export class ChangeMaxRowsAction extends EditDataAction {
private static EnabledClass = '';
public static ID = 'changeMaxRowsAction';
constructor(editor: EditDataEditor,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService
) {
super(editor, ChangeMaxRowsAction.ID, undefined, _connectionManagementService);
this.enabled = false;
this.class = ChangeMaxRowsAction.EnabledClass;
}
public run(): Promise<void> {
return Promise.resolve(null);
}
}
/*
* Action item that handles the dropdown (combobox) that lists the avaliable number of row selections
* for an edit data session
*/
export class ChangeMaxRowsActionItem extends Disposable implements IActionViewItem {
public actionRunner: IActionRunner;
public defaultRowCount: number;
private container: HTMLElement;
private start: HTMLElement;
private selectBox: SelectBox;
private _options: string[];
private _currentOptionsIndex: number;
constructor(
private _editor: EditDataEditor,
@IContextViewService contextViewService: IContextViewService,
@IThemeService private _themeService: IThemeService) {
super();
this._options = ['200', '1000', '10000'];
this._currentOptionsIndex = 0;
this.selectBox = new SelectBox(this._options, this._options[this._currentOptionsIndex], contextViewService);
this._registerListeners();
this._refreshOptions();
this.defaultRowCount = Number(this._options[this._currentOptionsIndex]);
this._register(attachSelectBoxStyler(this.selectBox, _themeService));
}
public render(container: HTMLElement): void {
this.container = container;
this.selectBox.render(dom.append(container, $('.configuration.listDatabasesSelectBox')));
}
public setActionContext(context: any): void {
}
public isEnabled(): boolean {
return true;
}
public enable(): void {
this.selectBox.enable();
}
public disable(): void {
this.selectBox.disable();
}
public set setCurrentOptionIndex(selection: number) {
this._currentOptionsIndex = firstIndex(this._options, x => x === selection.toString());
this._refreshOptions();
}
public focus(): void {
this.start.focus();
}
public blur(): void {
this.container.blur();
}
private _refreshOptions(databaseIndex?: number): void {
this.selectBox.setOptions(this._options, this._currentOptionsIndex);
}
private _registerListeners(): void {
this._register(this.selectBox.onDidSelect(selection => {
this._currentOptionsIndex = firstIndex(this._options, x => x === selection.selected);
this._editor.editDataInput.onRowDropDownSet(Number(selection.selected));
}));
this._register(attachSelectBoxStyler(this.selectBox, this._themeService));
}
}
/**
* Action class that is tied with toggling the Query editor
*/
export class ShowQueryPaneAction extends EditDataAction {
private static EnabledClass = 'filterLabel';
public static ID = 'showQueryPaneAction';
private readonly showSqlLabel = nls.localize('editData.showSql', "Show SQL Pane");
private readonly closeSqlLabel = nls.localize('editData.closeSql', "Close SQL Pane");
constructor(editor: EditDataEditor,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService
) {
super(editor, ShowQueryPaneAction.ID, ShowQueryPaneAction.EnabledClass, _connectionManagementService);
this.label = this.showSqlLabel;
}
public set queryPaneEnabled(value: boolean) {
this.updateLabel(value);
}
private updateLabel(queryPaneEnabled: boolean): void {
if (queryPaneEnabled) {
this.label = this.closeSqlLabel;
} else {
this.label = this.showSqlLabel;
}
}
public run(): Promise<void> {
this.editor.toggleQueryPane();
this.updateLabel(this.editor.queryPaneEnabled());
return Promise.resolve(null);
}
}

View File

@@ -0,0 +1,708 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as strings from 'vs/base/common/strings';
import * as DOM from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import { EditorOptions, EditorInput, IEditorControl, IEditor } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditDataInput } from 'sql/workbench/contrib/editData/browser/editDataInput';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as queryContext from 'sql/workbench/contrib/query/common/queryContext';
import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar';
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { IEditorDescriptorService } from 'sql/workbench/services/queryEditor/browser/editorDescriptorService';
import {
RefreshTableAction, StopRefreshTableAction, ChangeMaxRowsAction, ChangeMaxRowsActionItem, ShowQueryPaneAction
} from 'sql/workbench/contrib/editData/browser/editDataActions';
import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IFlexibleSash, HorizontalFlexibleSash } from 'sql/workbench/contrib/query/browser/flexibleSash';
import { EditDataResultsEditor } from 'sql/workbench/contrib/editData/browser/editDataResultsEditor';
import { EditDataResultsInput } from 'sql/workbench/contrib/editData/browser/editDataResultsInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
/**
* Editor that hosts an action bar and a resultSetInput for an edit data session
*/
export class EditDataEditor extends BaseEditor {
public static ID: string = 'workbench.editor.editDataEditor';
// The minimum width/height of the editors hosted in the QueryEditor
private readonly _minEditorSize: number = 220;
private _sash: IFlexibleSash;
private _dimension: DOM.Dimension;
private _resultsEditor: EditDataResultsEditor;
private _resultsEditorContainer: HTMLElement;
private _sqlEditor: TextResourceEditor;
private _sqlEditorContainer: HTMLElement;
private _taskbar: Taskbar;
private _taskbarContainer: HTMLElement;
private _changeMaxRowsActionItem: ChangeMaxRowsActionItem;
private _stopRefreshTableAction: StopRefreshTableAction;
private _refreshTableAction: RefreshTableAction;
private _changeMaxRowsAction: ChangeMaxRowsAction;
private _showQueryPaneAction: ShowQueryPaneAction;
private _spinnerElement: HTMLElement;
private _initialized: boolean = false;
private _queryEditorVisible: IContextKey<boolean>;
private hideQueryResultsView = false;
constructor(
@ITelemetryService _telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IEditorService private _editorService: IEditorService,
@IQueryModelService private _queryModelService: IQueryModelService,
@IEditorDescriptorService private _editorDescriptorService: IEditorDescriptorService,
@IContextKeyService contextKeyService: IContextKeyService,
@IStorageService storageService: IStorageService
) {
super(EditDataEditor.ID, _telemetryService, themeService, storageService);
if (contextKeyService) {
this._queryEditorVisible = queryContext.QueryEditorVisibleContext.bindTo(contextKeyService);
}
if (_editorService) {
_editorService.overrideOpenEditor((editor, options, group) => {
if (this.isVisible() && (editor !== this.input || group !== this.group)) {
this.saveEditorViewState();
}
return {};
});
}
}
// PUBLIC METHODS ////////////////////////////////////////////////////////////
// Getters and Setters
public get editDataInput(): EditDataInput { return <EditDataInput>this.input; }
public get tableName(): string { return this.editDataInput.tableName; }
public get uri(): string { return this.input ? this.editDataInput.uri.toString() : undefined; }
public set resultsEditorVisibility(isVisible: boolean) {
let input: EditDataInput = <EditDataInput>this.input;
input.results.visible = isVisible;
}
/**
* Called to indicate to the editor that the input should be cleared and resources associated with the
* input should be freed.
*/
public clearInput(): void {
if (this._resultsEditor) {
this._resultsEditor.clearInput();
}
if (this._sqlEditor) {
this._sqlEditor.clearInput();
}
this._disposeEditors();
super.clearInput();
}
public close(): void {
this.editDataInput.close();
}
/**
* Called to create the editor in the parent element.
*/
protected createEditor(parent: HTMLElement): void {
parent.style.position = 'absolute';
parent.style.height = '100%';
parent.style.width = '100%';
this._createTaskbar(parent);
}
public dispose(): void {
this._disposeEditors();
super.dispose();
}
/**
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
*/
public focus(): void {
if (this._sqlEditor) {
this._sqlEditor.focus();
}
}
public getControl(): IEditorControl {
if (this._sqlEditor) {
return this._sqlEditor.getControl();
}
return null;
}
public getEditorText(): string {
if (this._sqlEditor && this._sqlEditor.getControl()) {
let control = this._sqlEditor.getControl();
let codeEditor: ICodeEditor = <ICodeEditor>control;
if (codeEditor) {
let value = codeEditor.getModel().getValue();
if (value !== undefined && value.length > 0) {
return value;
}
}
}
return '';
}
/**
* Hide the spinner element to show that something was happening, hidden by default
*/
public hideSpinner(): void {
this._spinnerElement.style.visibility = 'hidden';
}
/**
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
* To be called when the container of this editor changes size.
*/
public layout(dimension: DOM.Dimension): void {
this._dimension = dimension;
if (this._sash) {
this._setSashDimension();
this._sash.layout();
}
this._doLayout();
this._resizeGridContents();
}
/**
* Sets this editor and the sub-editors to visible.
*/
public setEditorVisible(visible: boolean, group: IEditorGroup): void {
if (this._resultsEditor) {
this._resultsEditor.setVisible(visible, group);
}
if (this._sqlEditor) {
this._sqlEditor.setVisible(visible, group);
}
super.setEditorVisible(visible, group);
// Note: must update after calling super.setEditorVisible so that the accurate count is handled
this._updateQueryEditorVisible(visible);
}
/**
* Sets the input data for this editor.
*/
public setInput(newInput: EditDataInput, options?: EditorOptions): Promise<void> {
let oldInput = <EditDataInput>this.input;
if (!newInput.setup) {
this._initialized = false;
this._register(newInput.updateTaskbarEvent((owner) => this._updateTaskbar(owner)));
this._register(newInput.editorInitializingEvent((initializing) => this._onEditorInitializingChanged(initializing)));
this._register(newInput.showResultsEditorEvent(() => this._showResultsEditor()));
newInput.onRowDropDownSet(this._changeMaxRowsActionItem.defaultRowCount);
newInput.setupComplete();
}
return super.setInput(newInput, options, CancellationToken.None)
.then(() => this._updateInput(oldInput, newInput, options));
}
/**
* Show the spinner element that shows something is happening, hidden by default
*/
public showSpinner(): void {
setTimeout(() => {
if (!this._initialized) {
this._spinnerElement.style.visibility = 'visible';
}
}, 200);
}
public toggleResultsEditorVisibility(): void {
let input = <EditDataInput>this.input;
let hideResults = this.hideQueryResultsView;
this.hideQueryResultsView = !this.hideQueryResultsView;
if (!input.results) {
return;
}
this.resultsEditorVisibility = hideResults;
this._doLayout();
}
// PRIVATE METHODS ////////////////////////////////////////////////////////////
private _createEditor(editorInput: EditorInput, container: HTMLElement): Promise<BaseEditor> {
const descriptor = this._editorDescriptorService.getEditor(editorInput);
if (!descriptor) {
return Promise.reject(new Error(strings.format('Can not find a registered editor for the input {0}', editorInput)));
}
let editor = descriptor.instantiate(this._instantiationService);
editor.create(container);
editor.setVisible(this.isVisible(), editor.group);
return Promise.resolve(editor);
}
/**
* Appends the HTML for the EditDataResultsEditor to the EditDataEditor. If the HTML has not yet been
* created, it creates it and appends it. If it has already been created, it locates it and
* appends it.
*/
private _createResultsEditorContainer() {
this._createSash();
const parentElement = this.getContainer();
let input = <EditDataInput>this.input;
if (!input.results.container) {
this._resultsEditorContainer = DOM.append(parentElement, DOM.$('.editDataContainer-horizontal'));
input.results.container = this._resultsEditorContainer;
} else {
this._resultsEditorContainer = DOM.append(parentElement, input.results.container);
}
}
/**
* Creates the sash with the requested orientation and registers sash callbacks
*/
private _createSash(): void {
if (!this._sash) {
let parentElement: HTMLElement = this.getContainer();
this._sash = this._register(new HorizontalFlexibleSash(parentElement, this._minEditorSize));
this._setSashDimension();
this._register(this._sash.onPositionChange(position => this._doLayout()));
}
this._sash.show();
}
/**
* Appends the HTML for the SQL editor. Creates new HTML every time.
*/
private _createSqlEditorContainer() {
const parentElement = this.getContainer();
this._sqlEditorContainer = DOM.append(parentElement, DOM.$('.details-editor-container'));
}
private _createTaskbar(parentElement: HTMLElement): void {
// Create QueryTaskbar
this._taskbarContainer = DOM.append(parentElement, DOM.$('div'));
this._taskbar = new Taskbar(this._taskbarContainer, {
actionViewItemProvider: (action: Action) => this._getChangeMaxRowsAction(action)
});
// Create Actions for the toolbar
this._refreshTableAction = this._instantiationService.createInstance(RefreshTableAction, this);
this._stopRefreshTableAction = this._instantiationService.createInstance(StopRefreshTableAction, this);
this._changeMaxRowsAction = this._instantiationService.createInstance(ChangeMaxRowsAction, this);
this._showQueryPaneAction = this._instantiationService.createInstance(ShowQueryPaneAction, this);
// Create HTML Elements for the taskbar
let separator = Taskbar.createTaskbarSeparator();
let textSeparator = Taskbar.createTaskbarText(nls.localize('maxRowTaskbar', "Max Rows:"));
this._spinnerElement = Taskbar.createTaskbarSpinner();
// Set the content in the order we desire
let content: ITaskbarContent[] = [
{ action: this._refreshTableAction },
{ action: this._stopRefreshTableAction },
{ element: separator },
{ element: textSeparator },
{ action: this._changeMaxRowsAction },
{ action: this._showQueryPaneAction },
{ element: this._spinnerElement }
];
this._taskbar.setContent(content);
}
/**
* Gets the IActionItem for the list of row number drop down
*/
private _getChangeMaxRowsAction(action: Action): IActionViewItem {
let actionID = ChangeMaxRowsAction.ID;
if (action.id === actionID) {
if (!this._changeMaxRowsActionItem) {
this._changeMaxRowsActionItem = this._instantiationService.createInstance(ChangeMaxRowsActionItem, this);
}
return this._changeMaxRowsActionItem;
}
return null;
}
private _disposeEditors(): void {
if (this._sqlEditor) {
this._sqlEditor.dispose();
this._sqlEditor = null;
}
if (this._resultsEditor) {
this._resultsEditor.dispose();
this._resultsEditor = null;
}
let thisEditorParent: HTMLElement = this.getContainer();
if (this._sqlEditorContainer) {
let sqlEditorParent: HTMLElement = this._sqlEditorContainer.parentElement;
if (sqlEditorParent && sqlEditorParent === thisEditorParent) {
this._sqlEditorContainer.parentElement.removeChild(this._sqlEditorContainer);
}
this._sqlEditorContainer = null;
}
if (this._resultsEditorContainer) {
let resultsEditorParent: HTMLElement = this._resultsEditorContainer.parentElement;
if (resultsEditorParent && resultsEditorParent === thisEditorParent) {
this._resultsEditorContainer.parentElement.removeChild(this._resultsEditorContainer);
}
this._resultsEditorContainer = null;
this.hideQueryResultsView = false;
}
}
private _doLayout(skipResizeGridContent: boolean = false): void {
if (!this._isResultsEditorVisible() && this._sqlEditor) {
this._doLayoutSql();
return;
}
if (!this._sqlEditor || !this._resultsEditor || !this._dimension || !this._sash) {
return;
}
this._doLayoutHorizontal();
if (!skipResizeGridContent) {
this._resizeGridContents();
}
}
private _doLayoutHorizontal(): void {
let sqlEditorHeight: number;
let resultsEditorHeight: number;
this._resultsEditorContainer.hidden = false;
if (this.queryPaneEnabled()) {
const splitPointTop: number = this._sash.getSplitPoint();
this._sqlEditorContainer.hidden = false;
sqlEditorHeight = splitPointTop - this._getTaskBarHeight();
resultsEditorHeight = this._dimension.height - this._getTaskBarHeight() - sqlEditorHeight;
} else {
this._sqlEditorContainer.hidden = true;
sqlEditorHeight = 0;
resultsEditorHeight = this._dimension.height - this._getTaskBarHeight();
}
this._sqlEditorContainer.style.height = `${sqlEditorHeight}px`;
this._sqlEditorContainer.style.width = `${this._dimension.width}px`;
this._resultsEditorContainer.style.height = `${resultsEditorHeight}px`;
this._resultsEditorContainer.style.width = `${this._dimension.width}px`;
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, sqlEditorHeight));
this._resultsEditor.layout(new DOM.Dimension(this._dimension.width, resultsEditorHeight));
}
private _doLayoutSql() {
if (this._resultsEditorContainer) {
this._resultsEditorContainer.style.width = '0px';
this._resultsEditorContainer.style.height = '0px';
this._resultsEditorContainer.style.left = '0px';
this._resultsEditorContainer.hidden = true;
}
if (this._dimension) {
let sqlEditorHeight: number;
if (this.queryPaneEnabled()) {
this._sqlEditorContainer.hidden = false;
sqlEditorHeight = this._dimension.height - this._getTaskBarHeight();
} else {
this._sqlEditorContainer.hidden = true;
sqlEditorHeight = 0;
}
this._sqlEditorContainer.style.height = `${sqlEditorHeight}px`;
this._sqlEditorContainer.style.width = `${this._dimension.width}px`;
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, sqlEditorHeight));
}
}
private _getTaskBarHeight(): number {
let taskBarElement = this._taskbar.getContainer();
return DOM.getContentHeight(taskBarElement);
}
/**
* Returns true if the results table for the current edit data session is visible
* Public for testing only.
*/
private _isResultsEditorVisible(): boolean {
let input: EditDataInput = <EditDataInput>this.input;
if (!input) {
return false;
}
return input.results.visible;
}
private _onEditorInitializingChanged(initializing: boolean): void {
if (initializing) {
this.showSpinner();
} else {
this._initialized = true;
this.hideSpinner();
}
}
/**
* Sets input for the results editor after it has been created.
*/
private _onResultsEditorCreated(resultsEditor: EditDataResultsEditor, resultsInput: EditDataResultsInput, options: EditorOptions): Promise<void> {
this._resultsEditor = resultsEditor;
return this._resultsEditor.setInput(resultsInput, options);
}
/**
* Sets input for the SQL editor after it has been created.
*/
private _onSqlEditorCreated(sqlEditor: TextResourceEditor, sqlInput: UntitledEditorInput, options: EditorOptions): Thenable<void> {
this._sqlEditor = sqlEditor;
return this._sqlEditor.setInput(sqlInput, options, CancellationToken.None);
}
private _resizeGridContents(): void {
if (this._isResultsEditorVisible()) {
let queryInput: EditDataInput = <EditDataInput>this.input;
let uri: string = queryInput.uri;
if (uri) {
this._queryModelService.resizeResultsets(uri);
}
}
}
/**
* Handles setting input and creating editors when this QueryEditor is either:
* - Opened for the first time
* - Opened with a new EditDataInput
*/
private _setNewInput(newInput: EditDataInput, options?: EditorOptions): Promise<any> {
// Promises that will ensure proper ordering of editor creation logic
let createEditors: () => Promise<any>;
let onEditorsCreated: (result) => Promise<any>;
// If both editors exist, create joined promises - one for each editor
if (this._isResultsEditorVisible()) {
createEditors = () => {
return Promise.all([
this._createEditor(<EditDataResultsInput>newInput.results, this._resultsEditorContainer),
this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer)
]);
};
onEditorsCreated = (result: IEditor[]) => {
return Promise.all([
this._onResultsEditorCreated(<EditDataResultsEditor>result[0], newInput.results, options),
this._onSqlEditorCreated(<TextResourceEditor>result[1], newInput.sql, options)
]);
};
// If only the sql editor exists, create a promise and wait for the sql editor to be created
} else {
createEditors = () => {
return this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer);
};
onEditorsCreated = (result: TextResourceEditor) => {
return Promise.all([
this._onSqlEditorCreated(result, newInput.sql, options)
]);
};
}
// Create a promise to re render the layout after the editor creation logic
let doLayout: () => Promise<any> = () => {
this._doLayout();
return Promise.resolve(undefined);
};
// Run all three steps synchronously
return createEditors()
.then(onEditorsCreated)
.then(doLayout)
.then(() => {
if (newInput.results) {
newInput.results.onRestoreViewStateEmitter.fire();
}
if (newInput.savedViewState) {
this._sqlEditor.getControl().restoreViewState(newInput.savedViewState);
}
});
}
private _setSashDimension(): void {
if (!this._dimension) {
return;
}
this._sash.setDimenesion(this._dimension);
}
/**
* Makes visible the results table for the current edit data session
*/
private _showResultsEditor(): void {
if (this._isResultsEditorVisible()) {
return;
}
//this._editorGroupService.pinEditor(this.position, this.input);
let input = <EditDataInput>this.input;
this._createResultsEditorContainer();
this._createEditor(<EditDataResultsInput>input.results, this._resultsEditorContainer)
.then(result => {
this._onResultsEditorCreated(<EditDataResultsEditor>result, input.results, this.options);
this.resultsEditorVisibility = true;
this.hideQueryResultsView = false;
this._doLayout(true);
});
}
/**
* Handles setting input for this editor. If this new input does not match the old input (e.g. a new file
* has been opened with the same editor, or we are opening the editor for the first time).
*/
private _updateInput(oldInput: EditDataInput, newInput: EditDataInput, options?: EditorOptions): Promise<void> {
if (this._sqlEditor) {
this._sqlEditor.clearInput();
}
if (oldInput) {
this._disposeEditors();
}
this._createSqlEditorContainer();
if (this._isResultsEditorVisible()) {
this._createResultsEditorContainer();
let uri: string = newInput.uri;
if (uri) {
this._queryModelService.refreshResultsets(uri);
}
}
if (this._sash) {
if (this._isResultsEditorVisible()) {
this._sash.show();
} else {
this._sash.hide();
}
}
this._updateTaskbar(newInput);
return this._setNewInput(newInput, options);
}
private _updateQueryEditorVisible(currentEditorIsVisible: boolean): void {
if (this._queryEditorVisible) {
let visible = currentEditorIsVisible;
if (!currentEditorIsVisible) {
// Current editor is closing but still tracked as visible. Check if any other editor is visible
const candidates = [...this._editorService.visibleControls].filter(e => {
if (e && e.getId) {
return e.getId() === EditDataEditor.ID;
}
return false;
});
// Note: require 2 or more candidates since current is closing but still
// counted as visible
visible = candidates.length > 1;
}
this._queryEditorVisible.set(visible);
}
}
private _updateTaskbar(owner: EditDataInput): void {
// Update the taskbar if the owner of this call is being presented
if (owner.matches(this.editDataInput)) {
this._refreshTableAction.enabled = owner.refreshButtonEnabled;
this._stopRefreshTableAction.enabled = owner.stopButtonEnabled;
this._changeMaxRowsActionItem.setCurrentOptionIndex = owner.rowLimit;
this._showQueryPaneAction.queryPaneEnabled = owner.queryPaneEnabled;
}
}
/**
* Calls the run method of this editor's RunQueryAction
*/
public runQuery(): void {
this._refreshTableAction.run();
}
/**
* Calls the run method of this editor's CancelQueryAction
*/
public cancelQuery(): void {
this._stopRefreshTableAction.run();
}
public toggleQueryPane(): void {
this.editDataInput.queryPaneEnabled = !this.queryPaneEnabled();
if (this.queryPaneEnabled()) {
this._showQueryEditor();
} else {
this._hideQueryEditor();
}
this._doLayout(false);
}
private _showQueryEditor(): void {
this._sqlEditorContainer.hidden = false;
this._changeMaxRowsActionItem.disable();
}
private _hideQueryEditor(): void {
this._sqlEditorContainer.hidden = true;
this._changeMaxRowsActionItem.enable();
}
public queryPaneEnabled(): boolean {
return this.editDataInput.queryPaneEnabled;
}
private saveEditorViewState(): void {
let editDataInput = this.input as EditDataInput;
if (editDataInput) {
if (this._sqlEditor) {
editDataInput.savedViewState = this._sqlEditor.getControl().saveViewState();
}
if (editDataInput.results) {
editDataInput.results.onSaveViewStateEmitter.fire();
}
}
}
}

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.
*--------------------------------------------------------------------------------------------*/
import { IGridInfo } from 'sql/workbench/contrib/grid/common/interfaces';
import { DataService } from 'sql/workbench/contrib/grid/common/dataService';
import { GridActionProvider } from 'sql/workbench/contrib/editData/common/gridActions';
import { localize } from 'vs/nls';
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: () => void
) {
super(dataService, selectAllCallback);
}
/**
* Return actions given a click on an edit data grid
*/
public getGridActions(): 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 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): Promise<boolean> {
this.callback(gridInfo.rowIndex);
return Promise.resolve(true);
}
}
export class RevertRowAction extends Action {
public static ID = 'grid.revertRow';
public static LABEL = localize('revertRow', "Revert Current Row");
constructor(
id: string,
label: string,
private callback: () => void
) {
super(id, label);
}
public run(gridInfo: IGridInfo): Promise<boolean> {
this.callback();
return Promise.resolve(true);
}
}

View File

@@ -0,0 +1,239 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput, EditorModel, ConfirmResult, EncodingMode } from 'vs/workbench/common/editor';
import { IConnectionManagementService, IConnectableInput, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { Event, Emitter } from 'vs/base/common/event';
import { EditSessionReadyParams } from 'azdata';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { EditDataResultsInput } from 'sql/workbench/contrib/editData/browser/editDataResultsInput';
import { IEditorViewState } from 'vs/editor/common/editorCommon';
/**
* Input for the EditDataEditor.
*/
export class EditDataInput extends EditorInput implements IConnectableInput {
public static ID: string = 'workbench.editorinputs.editDataInput';
private _hasBootstrapped: boolean;
private _editorContainer: HTMLElement;
private _updateTaskbar: Emitter<EditDataInput>;
private _editorInitializing: Emitter<boolean>;
private _showResultsEditor: Emitter<EditDataInput>;
private _refreshButtonEnabled: boolean;
private _stopButtonEnabled: boolean;
private _setup: boolean;
private _rowLimit: number;
private _objectType: string;
private _css: HTMLStyleElement;
private _useQueryFilter: boolean;
public savedViewState: IEditorViewState;
constructor(
private _uri: URI,
private _schemaName,
private _tableName,
private _sql: UntitledEditorInput,
private _queryString: string,
private _results: EditDataResultsInput,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IQueryModelService private _queryModelService: IQueryModelService,
@INotificationService private notificationService: INotificationService
) {
super();
this._hasBootstrapped = false;
this._updateTaskbar = new Emitter<EditDataInput>();
this._showResultsEditor = new Emitter<EditDataInput>();
this._editorInitializing = new Emitter<boolean>();
this._setup = false;
this._stopButtonEnabled = false;
this._refreshButtonEnabled = false;
this._useQueryFilter = false;
// re-emit sql editor events through this editor if it exists
if (this._sql) {
this._register(this._sql.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
this._sql.disableSaving();
}
this.disableSaving();
//TODO determine is this is a table or a view
this._objectType = 'TABLE';
// Attach to event callbacks
if (this._queryModelService) {
let self = this;
// Register callbacks for the Actions
this._register(
this._queryModelService.onRunQueryStart(uri => {
if (self.uri === uri) {
self.initEditStart();
}
})
);
this._register(
this._queryModelService.onEditSessionReady((result) => {
if (self.uri === result.ownerUri) {
self.initEditEnd(result);
}
})
);
}
}
// Getters/Setters
public get tableName(): string { return this._tableName; }
public get schemaName(): string { return this._schemaName; }
public get uri(): string { return this._uri.toString(); }
public get sql(): UntitledEditorInput { return this._sql; }
public get results(): EditDataResultsInput { return this._results; }
public getResultsInputResource(): string { return this._results.uri; }
public get updateTaskbarEvent(): Event<EditDataInput> { return this._updateTaskbar.event; }
public get editorInitializingEvent(): Event<boolean> { return this._editorInitializing.event; }
public get showResultsEditorEvent(): Event<EditDataInput> { return this._showResultsEditor.event; }
public get stopButtonEnabled(): boolean { return this._stopButtonEnabled; }
public get refreshButtonEnabled(): boolean { return this._refreshButtonEnabled; }
public get container(): HTMLElement { return this._editorContainer; }
public get hasBootstrapped(): boolean { return this._hasBootstrapped; }
public get setup(): boolean { return this._setup; }
public get rowLimit(): number { return this._rowLimit; }
public get objectType(): string { return this._objectType; }
public showResultsEditor(): void { this._showResultsEditor.fire(undefined); }
public isDirty(): boolean { return false; }
public save(): Promise<boolean> { return Promise.resolve(false); }
public confirmSave(): Promise<ConfirmResult> { return Promise.resolve(ConfirmResult.DONT_SAVE); }
public getTypeId(): string { return EditDataInput.ID; }
public setBootstrappedTrue(): void { this._hasBootstrapped = true; }
public getResource(): URI { return this._uri; }
public supportsSplitEditor(): boolean { return false; }
public setupComplete() { this._setup = true; }
public get queryString(): string {
return this._queryString;
}
public set queryString(queryString: string) {
this._queryString = queryString;
}
public get css(): HTMLStyleElement {
return this._css;
}
public set css(css: HTMLStyleElement) {
this._css = css;
}
public get queryPaneEnabled(): boolean {
return this._useQueryFilter;
}
public set queryPaneEnabled(useQueryFilter: boolean) {
this._useQueryFilter = useQueryFilter;
}
// State Update Callbacks
public initEditStart(): void {
this._editorInitializing.fire(true);
this._refreshButtonEnabled = false;
this._stopButtonEnabled = true;
this._updateTaskbar.fire(this);
}
public initEditEnd(result: EditSessionReadyParams): void {
this._refreshButtonEnabled = true;
this._stopButtonEnabled = false;
if (!result.success) {
this.notificationService.notify({
severity: Severity.Error,
message: result.message
});
}
this._editorInitializing.fire(false);
this._updateTaskbar.fire(this);
}
public onConnectStart(): void {
// TODO: Indicate connection started
}
public onConnectReject(error?: string): void {
if (error) {
this.notificationService.notify({
severity: Severity.Error,
message: nls.localize('connectionFailure', "Edit Data Session Failed To Connect")
});
}
}
public onConnectCanceled(): void {
}
public onConnectSuccess(params?: INewConnectionParams): void {
let rowLimit: number = undefined;
let queryString: string = undefined;
if (this._useQueryFilter) {
queryString = this._queryString;
} else {
rowLimit = this._rowLimit;
}
this._queryModelService.initializeEdit(this.uri, this.schemaName, this.tableName, this._objectType, rowLimit, queryString);
this.showResultsEditor();
this._onDidChangeLabel.fire();
}
public onDisconnect(): void {
// TODO: deal with disconnections
}
public onRowDropDownSet(rows: number) {
this._rowLimit = rows;
}
// Boiler Plate Functions
public matches(otherInput: any): boolean {
if (otherInput instanceof EditDataInput) {
return this._sql.matches(otherInput.sql);
}
return this._sql.matches(otherInput);
}
public dispose(): void {
this._queryModelService.disposeQuery(this.uri);
this._sql.dispose();
this._results.dispose();
super.dispose();
}
public close(): void {
// Dispose our edit session then disconnect our input
this._queryModelService.disposeEdit(this.uri).then(() => {
return this._connectionManagementService.disconnectEditor(this, true);
}).then(() => {
this.dispose();
});
}
public get tabColor(): string {
return this._connectionManagementService.getTabColorForUri(this.uri);
}
public get onDidModelChangeContent(): Event<void> { return this._sql.onDidModelChangeContent; }
public get onDidModelChangeEncoding(): Event<void> { return this._sql.onDidModelChangeEncoding; }
public resolve(refresh?: boolean): Promise<EditorModel> { return this._sql.resolve(); }
public getEncoding(): string { return this._sql.getEncoding(); }
public suggestFileName(): string { return this._sql.suggestFileName(); }
public getName(): string { return this._sql.getName(); }
public get hasAssociatedFilePath(): boolean { return this._sql.hasAssociatedFilePath; }
public setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void {
this._sql.setEncoding(encoding, mode);
}
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorOptions } from 'vs/workbench/common/editor';
import { getZoomLevel } from 'vs/base/browser/browser';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import * as types from 'vs/base/common/types';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService';
import { BareResultsGridInfo } from 'sql/workbench/contrib/query/browser/queryResultsEditor';
import { IEditDataComponentParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import { EditDataModule } from 'sql/workbench/contrib/editData/browser/editData.module';
import { EDITDATA_SELECTOR } from 'sql/workbench/contrib/editData/browser/editData.component';
import { EditDataResultsInput } from 'sql/workbench/contrib/editData/browser/editDataResultsInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class EditDataResultsEditor extends BaseEditor {
public static ID: string = 'workbench.editor.editDataResultsEditor';
public static AngularSelectorString: string = 'slickgrid-container.slickgridContainer';
protected _input: EditDataResultsInput;
protected _rawOptions: BareResultsGridInfo;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConfigurationService private _configurationService: IConfigurationService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService
) {
super(EditDataResultsEditor.ID, telemetryService, themeService, storageService);
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();
}
});
}
public get input(): EditDataResultsInput {
return this._input;
}
public createEditor(parent: HTMLElement): void {
}
public dispose(): void {
super.dispose();
}
public layout(dimension: DOM.Dimension): void {
}
public setInput(input: EditDataResultsInput, options: EditorOptions): Promise<void> {
super.setInput(input, options, CancellationToken.None);
this._applySettings();
if (!input.hasBootstrapped) {
this._bootstrapAngular();
}
return Promise.resolve<void>(null);
}
private _applySettings() {
if (this.input && this.input.container) {
Configuration.applyFontInfoSlow(this.getContainer(), this._rawOptions);
if (!this.input.css) {
this.input.css = DOM.createStyleSheet(this.input.container);
}
let cssRuleText = '';
if (types.isNumber(this._rawOptions.cellPadding)) {
cssRuleText = this._rawOptions.cellPadding + 'px';
} else {
cssRuleText = this._rawOptions.cellPadding.join('px ') + 'px;';
}
let content = `.grid .slick-cell { padding: ${cssRuleText}; }`;
this.input.css.innerHTML = content;
}
}
/**
* Load the angular components and record for this input that we have done so
*/
private _bootstrapAngular(): void {
let input = <EditDataResultsInput>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
const parent = input.container;
let params: IEditDataComponentParams = {
dataService: dataService,
onSaveViewState: input.onSaveViewStateEmitter.event,
onRestoreViewState: input.onRestoreViewStateEmitter.event
};
bootstrapAngular(this._instantiationService,
EditDataModule,
parent,
EDITDATA_SELECTOR,
params,
input);
}
}

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 { EditorInput } from 'vs/workbench/common/editor';
import { Emitter } from 'vs/base/common/event';
/**
* Input for the EditDataResultsEditor. This input helps with logic for the viewing and editing of
* data in the results grid.
*/
export class EditDataResultsInput extends EditorInput {
// Tracks if the editor that holds this input should be visible (i.e. true if a query has been run)
private _visible: boolean;
// Tracks if the editor has holds this input has has bootstrapped angular yet
private _hasBootstrapped: boolean;
// Holds the HTML content for the editor when the editor discards this input and loads another
private _editorContainer: HTMLElement;
public css: HTMLStyleElement;
public readonly onRestoreViewStateEmitter = new Emitter<void>();
public readonly onSaveViewStateEmitter = new Emitter<void>();
constructor(private _uri: string) {
super();
this._visible = false;
this._hasBootstrapped = false;
}
getTypeId(): string {
return EditDataResultsInput.ID;
}
matches(other: any): boolean {
if (other instanceof EditDataResultsInput) {
return (other._uri === this._uri);
}
return false;
}
resolve(refresh?: boolean): Promise<any> {
return Promise.resolve(null);
}
supportsSplitEditor(): boolean {
return false;
}
public setBootstrappedTrue(): void {
this._hasBootstrapped = true;
}
public dispose(): void {
this._disposeContainer();
super.dispose();
}
private _disposeContainer() {
if (!this._editorContainer) {
return;
}
let parentContainer = this._editorContainer.parentNode;
if (parentContainer) {
parentContainer.removeChild(this._editorContainer);
this._editorContainer = null;
}
}
//// Properties
static get ID() {
return 'workbench.editorinputs.editDataResultsInput';
}
set container(container: HTMLElement) {
this._disposeContainer();
this._editorContainer = container;
}
get container(): HTMLElement {
return this._editorContainer;
}
get hasBootstrapped(): boolean {
return this._hasBootstrapped;
}
get visible(): boolean {
return this._visible;
}
set visible(visible: boolean) {
this._visible = visible;
}
get uri(): string {
return this._uri;
}
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as GridContentEvents from 'sql/workbench/contrib/grid/common/gridContentEvents';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { QueryEditor } from 'sql/workbench/contrib/query/browser/queryEditor';
import { EditDataEditor } from 'sql/workbench/contrib/editData/browser/editDataEditor';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
function runActionOnActiveResultsEditor(accessor: ServicesAccessor, eventName: string): void {
let editorService = accessor.get(IEditorService);
const candidates = [editorService.activeControl, ...editorService.visibleControls].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 goToNextQueryOutputTab = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.GoToNextQueryOutputTab);
};
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 saveAsXml = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsXML);
};
export const selectAll = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SelectAll);
};
export const selectAllMessages = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.SelectAllMessages);
};
export const viewAsChart = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsChart);
};
export const viewAsVisualizer = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.ViewAsVisualizer);
};
export const goToNextGrid = (accessor: ServicesAccessor) => {
runActionOnActiveResultsEditor(accessor, GridContentEvents.GoToNextGrid);
};

View File

@@ -0,0 +1,520 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/flexbox';
import 'vs/css!./media/styles';
import { Subscription, Subject } from 'rxjs/Rx';
import { ElementRef, QueryList, ChangeDetectorRef, ViewChildren } from '@angular/core';
import { SlickGrid } from 'angular2-slickgrid';
import * as Constants from 'sql/workbench/contrib/query/common/constants';
import * as LocalizedConstants from 'sql/workbench/contrib/query/common/localizedConstants';
import { IGridInfo, IGridDataSet, SaveFormat } from 'sql/workbench/contrib/grid/common/interfaces';
import * as Utils from 'sql/platform/connection/common/utils';
import { DataService } from 'sql/workbench/contrib/grid/common/dataService';
import * as actions from 'sql/workbench/contrib/editData/common/gridActions';
import * as GridContentEvents from 'sql/workbench/contrib/grid/common/gridContentEvents';
import { ResultsVisibleContext, ResultsGridFocussedContext, ResultsMessagesFocussedContext, QueryEditorVisibleContext } from 'sql/workbench/contrib/query/common/queryContext';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
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 { DisposableStore } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ILogService } from 'vs/platform/log/common/log';
import { subscriptionToDisposable } from 'sql/base/browser/lifecycle';
export abstract class GridParentComponent {
// CONSTANTS
// tslint:disable:no-unused-variable
protected get selectionModel() { return new CellSelectionModel(); }
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 actionProvider: actions.GridActionProvider;
protected toDispose = new DisposableStore();
// Context keys to set when keybindings are available
private resultsVisibleContextKey: IContextKey<boolean>;
private gridFocussedContextKey: IContextKey<boolean>;
private messagesFocussedContextKey: IContextKey<boolean>;
private queryEditorVisible: 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>;
set messageActive(input: boolean) {
this._messageActive = input;
if (this.resultActive) {
this.resizeGrids();
}
this._cd.detectChanges();
}
get messageActive(): boolean {
return this._messageActive;
}
constructor(
protected _el: ElementRef,
protected _cd: ChangeDetectorRef,
protected contextMenuService: IContextMenuService,
protected keybindingService: IKeybindingService,
protected contextKeyService: IContextKeyService,
protected configurationService: IConfigurationService,
protected clipboardService: IClipboardService,
protected queryEditorService: IQueryEditorService,
protected logService: ILogService
) {
}
protected baseInit(): void {
const self = this;
this.initShortcutsBase();
if (this.configurationService) {
let sqlConfig = this.configurationService.getValue('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;
case GridContentEvents.SaveAsXML:
self.sendSaveRequest(SaveFormat.XML);
break;
case GridContentEvents.GoToNextQueryOutputTab:
self.goToNextQueryOutputTab();
break;
case GridContentEvents.ViewAsChart:
self.showChartForGrid(self.activeGrid);
break;
case GridContentEvents.ViewAsVisualizer:
self.showVisualizerForGrid(self.activeGrid);
break;
case GridContentEvents.GoToNextGrid:
self.goToNextGrid();
break;
default:
this.logService.error('Unexpected grid content event type "' + type + '" sent');
break;
}
});
this.bindKeys(this.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.add(subscriptionToDisposable(sub));
}
private bindKeys(contextKeyService: IContextKeyService): void {
if (contextKeyService) {
this.queryEditorVisible = QueryEditorVisibleContext.bindTo(contextKeyService);
this.queryEditorVisible.set(true);
let gridContextKeyService = this.contextKeyService.createScoped(this._el.nativeElement);
this.toDispose.add(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();
}
protected toggleResultPane(): void {
this.resultActive = !this.resultActive;
if (this.resultActive) {
this.resizeGrids();
}
this._cd.detectChanges();
}
protected 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);
}
protected getSelection(index?: number): Slick.Range[] {
let selection = this.slickgrids.toArray()[index || this.activeGrid].getSelectedRanges();
if (selection) {
selection = selection.map(c => { return <Slick.Range>{ fromCell: c.fromCell - 1, toCell: c.toCell - 1, toRow: c.toRow, fromRow: c.fromRow }; });
return selection;
} else {
return undefined;
}
}
private copySelection(): void {
let messageText = this.getMessageText();
if (messageText.length > 0) {
this.clipboardService.writeText(messageText);
} else {
let activeGrid = this.activeGrid;
this.dataService.copyResults(this.getSelection(activeGrid), this.renderedDataSets[activeGrid].batchId, this.renderedDataSets[activeGrid].resultId);
}
}
private copyWithHeaders(): void {
let activeGrid = this.activeGrid;
this.dataService.copyResults(this.getSelection(activeGrid), 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) {
this.clipboardService.writeText(messageText);
}
}
private getMessageText(): string {
if (document.activeElement === this.getMessagesElement()) {
if (window.getSelection()) {
return window.getSelection().toString();
}
}
return '';
}
protected goToNextQueryOutputTab(): void {
}
protected showChartForGrid(index: number) {
}
protected showVisualizerForGrid(index: number) {
}
protected goToNextGrid() {
if (this.renderedDataSets.length > 0) {
let next = this.activeGrid + 1;
if (next >= this.renderedDataSets.length) {
next = 0;
}
this.navigateToGrid(next);
}
}
protected navigateToGrid(index: number) {
}
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);
},
'SaveAsXML': () => {
this.sendSaveRequest(SaveFormat.XML);
},
'GoToNextQueryOutputTab': () => {
this.goToNextQueryOutputTab();
}
};
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: Slick.Range[] }): 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 'savexml':
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.XML, 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;
this.dataService.sendSaveRequest({ batchIndex: batchId, resultSetNumber: resultId, format: format, selection: this.getSelection(activeGrid) });
}
protected _keybindingFor(action: IAction): ResolvedKeybinding {
let [kb] = this.keybindingService.lookupKeybindings(action.id);
return kb;
}
openContextMenu(event, batchId, resultId, index): void {
let slick: any = this.slickgrids.toArray()[index];
let grid = slick._grid;
let selection = this.getSelection(index);
if (selection && selection.length === 0) {
let cell = (grid as Slick.Grid<any>).getCellFromEvent(event);
selection = [new Slick.Range(cell.row, cell.cell - 1)];
}
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),
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
*
* @memberOf QueryComponent
*/
protected onGridSelectAll(): (gridIndex: number) => void {
let self = this;
return (gridIndex: number) => {
self.activeGrid = gridIndex;
let grid = self.slickgrids.toArray()[self.activeGrid];
grid.setActive();
grid.selection = true;
};
}
private onSelectAllForActiveGrid(): void {
if (this.activeGrid >= 0 && this.slickgrids.length > this.activeGrid) {
this.slickgrids.toArray()[this.activeGrid].selection = true;
}
}
/**
* 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(): Selection {
if (document.activeElement === this.getMessagesElement()) {
return window.getSelection();
} else {
return undefined;
}
}
selectAllMessages(): void {
let msgEl = this._el.nativeElement.querySelector('#messages');
this.selectElementContents(msgEl);
}
selectElementContents(el: HTMLElement): void {
let range = document.createRange();
range.selectNodeContents(el);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
keyEvent(e: KeyboardEvent): void {
if (this.tryHandleKeyEvent(new StandardKeyboardEvent(e))) {
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.
*
* @memberOf GridParentComponent
*/
protected abstract tryHandleKeyEvent(e: StandardKeyboardEvent): boolean;
resizeGrids(): void {
const self = this;
setTimeout(() => {
for (let grid of self.renderedDataSets) {
grid.resized.emit();
}
});
}
// Private Helper Functions ////////////////////////////////////////////////////////////////////////////
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* 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;
}
#workbench\.editor\.editDataEditor .monaco-toolbar .monaco-select-box {
margin-top: 4px;
margin-bottom: 4px;
}

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.
*--------------------------------------------------------------------------------------------*/
.fullsize {
height: 100%;
width: 100%;
}
/* vertical box styles */
.vertBox {
display: flex;
flex-flow: column;
}
.vertBox .boxRow.header {
flex: 0 0 auto;
}
.vertBox .boxRow.content {
flex: 1 1 0;
}
.edgesPadding {
padding-left: 10px;
padding-right: 10px;
padding-top: 10px;
}
.messagesTopSpacing {
height: 5px;
}
.results {
flex: 100 1 0;
}
.scrollable {
overflow: auto;
}
.maxHeight {
max-height: fit-content;
}
.minHeight {
min-height: fit-content;
}
/* horizontal box style */
.horzBox {
display: flex;
flex-flow: row;
}
.horzBox .boxCol.content {
flex: 1 1 1%;
overflow: auto;
max-width: fit-content;
}
.hidden {
display: none !important;
}
/* testing
.vertBox {
border: 1px solid green;
}
.horzBox {
border: 1px solid red;
}
*/

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* slick grid */
.boxRow.content.horzBox.slickgrid {
margin-bottom: 2px;
}
.boxRow.content.horzBox.slickgrid:last-child {
margin-bottom: 0px;
}
.boxRow.content.padded {
padding: 15px
}
/* icon */
.gridIcon {
padding: 8px;
background-repeat: no-repeat;
background-position: 0px;
}
.gridIconContainer {
margin: 5px 5px;
}
/* messages */
.resultsMessageTable {
font-weight: inherit;
font-size: inherit;
table-layout: fixed;
border-collapse: collapse;
width: 100%;
user-select: initial;
-webkit-user-select: initial;
}
.wideResultsMessage {
width: 110px;
}
#messageTable td {
padding-right: 20px;
}
#messageTable tbody tr:last-child > td {
padding-bottom: 1em;
}
#messageTable {
-webkit-user-select: text;
user-select: text;
}
.resultsMessageValue {
white-space: pre-line;
}
/* headers */
.resultsMessageHeader {
height: auto;
overflow: hidden;
height: 22px;
padding-left: 20px;
line-height: 22px;
}
.resultsMessageHeader > span {
float: left;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.resizableHandle {
font-size: 0.1px;
display: block;
float: right;
position: absolute;
cursor: row-resize;
top: 0;
height: 4px;
width: 100%;
z-index: 1;
}
#resizeHandle {
font-size: 0.1px;
position: absolute;
cursor: row-resize;
height: 4px;
width: 100%;
z-index: 2;
background-color: var(--color-resize-handle);
}
.header > span.shortCut {
float: right;
padding-right: 5px;
}
.queryLink {
cursor: pointer;
text-decoration: underline;
}