mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Share Notebook grid rendering with Query editor (#6241)
This is a staged refactor to use the exact same grid logic in the Notebook and query editors, including context menu support, font settings, and sizing logic. The goal long term is: - As the core Query grid is updated, Notebook can benefit from the changes - As we add in support for contributions like new buttons & actions working on the grid, can share the logic - Ideally if and when we refactor things like the action bar for grid results, we can apply in both places though this is TBD. Fixes a number of issues: - Fixes #5755 Grids don't respond to font settings. @anthonydresser can we remove setting from each query results editor and just use Notebook Styles since these are global (not scoped) settings? - Fixes #5501 Copy from grid settings. - Fixes #4938 SQL Notebook result sets are missing the actions provide for SQL File results sets. this now has the core ability to solve this, and separate work items for specific asks (serialization, charting) are tracked. Currently hidden: - Save as... options in context menu - All right toolbar actions (save as, chart). Remaining issues to address in future commits: - Need to implement support for serialization (#5137). - Need to add charting support - Need to solve the layout of buttons on the right hand side when a small number of columns are output. It doesn't look right that buttons are so far away from the results - Will work with UX on this. For now, mitigating this by hiding all buttons, but will need to solve in the future - Would like to make buttons contributable via extension, but need to refactor similar to ObjectExplorer context menu so that we can serialize context menu options across to extension host while still having internal actions with full support
This commit is contained in:
102
src/sql/platform/query/common/gridDataProvider.ts
Normal file
102
src/sql/platform/query/common/gridDataProvider.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as types from 'vs/base/common/types';
|
||||||
|
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||||
|
|
||||||
|
export interface IGridDataProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets N rows of data
|
||||||
|
* @param rowStart 0-indexed start row to retrieve data from
|
||||||
|
* @param numberOfRows total number of rows of data to retrieve
|
||||||
|
*/
|
||||||
|
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a copy request to copy data to the clipboard
|
||||||
|
* @param selection The selection range to copy
|
||||||
|
* @param batchId The batch id of the result to copy from
|
||||||
|
* @param resultId The result id of the result to copy from
|
||||||
|
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
|
||||||
|
*/
|
||||||
|
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the EOL terminator to use for this data type.
|
||||||
|
*/
|
||||||
|
getEolString(): string;
|
||||||
|
|
||||||
|
shouldIncludeHeaders(includeHeaders: boolean): boolean;
|
||||||
|
|
||||||
|
shouldRemoveNewLines(): boolean;
|
||||||
|
|
||||||
|
getColumnHeaders(range: Slick.Range): string[];
|
||||||
|
|
||||||
|
readonly canSerialize: boolean;
|
||||||
|
|
||||||
|
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResultsString(provider: IGridDataProvider, selection: Slick.Range[], includeHeaders?: boolean): Promise<string> {
|
||||||
|
let copyString = '';
|
||||||
|
const eol = provider.getEolString();
|
||||||
|
|
||||||
|
// create a mapping of the ranges to get promises
|
||||||
|
let tasks = selection.map((range, i) => {
|
||||||
|
return async () => {
|
||||||
|
const result = await provider.getRowData(range.fromRow, range.toRow - range.fromRow + 1);
|
||||||
|
// If there was a previous selection separate it with a line break. Currently
|
||||||
|
// when there are multiple selections they are never on the same line
|
||||||
|
if (i > 0) {
|
||||||
|
copyString += eol;
|
||||||
|
}
|
||||||
|
if (provider.shouldIncludeHeaders(includeHeaders)) {
|
||||||
|
let columnHeaders = provider.getColumnHeaders(range);
|
||||||
|
if (columnHeaders !== undefined) {
|
||||||
|
copyString += columnHeaders.join('\t') + eol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iterate over the rows to paste into the copy string
|
||||||
|
for (let rowIndex: number = 0; rowIndex < result.resultSubset.rows.length; rowIndex++) {
|
||||||
|
let row = result.resultSubset.rows[rowIndex];
|
||||||
|
let cellObjects = row.slice(range.fromCell, (range.toCell + 1));
|
||||||
|
// Remove newlines if requested
|
||||||
|
let cells = provider.shouldRemoveNewLines()
|
||||||
|
? cellObjects.map(x => removeNewLines(x.displayValue))
|
||||||
|
: cellObjects.map(x => x.displayValue);
|
||||||
|
copyString += cells.join('\t');
|
||||||
|
if (rowIndex < result.resultSubset.rows.length - 1) {
|
||||||
|
copyString += eol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tasks.length > 0) {
|
||||||
|
let p = tasks[0]();
|
||||||
|
for (let i = 1; i < tasks.length; i++) {
|
||||||
|
p = p.then(tasks[i]);
|
||||||
|
}
|
||||||
|
await p;
|
||||||
|
}
|
||||||
|
return copyString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function removeNewLines(inputString: string): string {
|
||||||
|
// This regex removes all newlines in all OS types
|
||||||
|
// Windows(CRLF): \r\n
|
||||||
|
// Linux(LF)/Modern MacOS: \n
|
||||||
|
// Old MacOs: \r
|
||||||
|
if (types.isUndefinedOrNull(inputString)) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputString: string = inputString.replace(/(\r\n|\n|\r)/gm, '');
|
||||||
|
return outputString;
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
|||||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||||
|
import { IGridDataProvider, getResultsString } from 'sql/platform/query/common/gridDataProvider';
|
||||||
|
import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||||
|
|
||||||
export interface IEditSessionReadyEvent {
|
export interface IEditSessionReadyEvent {
|
||||||
ownerUri: string;
|
ownerUri: string;
|
||||||
@@ -98,7 +100,6 @@ export default class QueryRunner extends Disposable {
|
|||||||
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
||||||
@INotificationService private _notificationService: INotificationService,
|
@INotificationService private _notificationService: INotificationService,
|
||||||
@IConfigurationService private _configurationService: IConfigurationService,
|
@IConfigurationService private _configurationService: IConfigurationService,
|
||||||
@IClipboardService private _clipboardService: IClipboardService,
|
|
||||||
@IInstantiationService private instantiationService: IInstantiationService,
|
@IInstantiationService private instantiationService: IInstantiationService,
|
||||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||||
) {
|
) {
|
||||||
@@ -212,7 +213,7 @@ export default class QueryRunner extends Disposable {
|
|||||||
|
|
||||||
private handleFailureRunQueryResult(error: any) {
|
private handleFailureRunQueryResult(error: any) {
|
||||||
// Attempting to launch the query failed, show the error message
|
// Attempting to launch the query failed, show the error message
|
||||||
const eol = this.getEolString();
|
const eol = getEolString(this._textResourcePropertiesService, this.uri);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
error = error.message;
|
error = error.message;
|
||||||
}
|
}
|
||||||
@@ -437,10 +438,7 @@ export default class QueryRunner extends Disposable {
|
|||||||
|
|
||||||
// TODO issue #228 add statusview callbacks here
|
// TODO issue #228 add statusview callbacks here
|
||||||
this._isExecuting = false;
|
this._isExecuting = false;
|
||||||
this._notificationService.notify({
|
this._notificationService.error(nls.localize('query.initEditExecutionFailed', "Initialize edit data session failed: ") + error);
|
||||||
severity: Severity.Error,
|
|
||||||
message: nls.localize('query.initEditExecutionFailed', 'Init Edit Execution failed: ') + error
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,76 +539,12 @@ export default class QueryRunner extends Disposable {
|
|||||||
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
|
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
|
||||||
*/
|
*/
|
||||||
copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||||
const self = this;
|
let provider = this.getGridDataProvider(batchId, resultId);
|
||||||
let copyString = '';
|
provider.copyResults(selection, includeHeaders);
|
||||||
const eol = this.getEolString();
|
|
||||||
|
|
||||||
// create a mapping of the ranges to get promises
|
|
||||||
let tasks = selection.map((range, i) => {
|
|
||||||
return () => {
|
|
||||||
return self.getQueryRows(range.fromRow, range.toRow - range.fromRow + 1, batchId, resultId).then((result) => {
|
|
||||||
// If there was a previous selection separate it with a line break. Currently
|
|
||||||
// when there are multiple selections they are never on the same line
|
|
||||||
if (i > 0) {
|
|
||||||
copyString += eol;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.shouldIncludeHeaders(includeHeaders)) {
|
|
||||||
let columnHeaders = self.getColumnHeaders(batchId, resultId, range);
|
|
||||||
if (columnHeaders !== undefined) {
|
|
||||||
copyString += columnHeaders.join('\t') + eol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over the rows to paste into the copy string
|
|
||||||
for (let rowIndex: number = 0; rowIndex < result.resultSubset.rows.length; rowIndex++) {
|
|
||||||
let row = result.resultSubset.rows[rowIndex];
|
|
||||||
let cellObjects = row.slice(range.fromCell, (range.toCell + 1));
|
|
||||||
// Remove newlines if requested
|
|
||||||
let cells = self.shouldRemoveNewLines()
|
|
||||||
? cellObjects.map(x => self.removeNewLines(x.displayValue))
|
|
||||||
: cellObjects.map(x => x.displayValue);
|
|
||||||
copyString += cells.join('\t');
|
|
||||||
if (rowIndex < result.resultSubset.rows.length - 1) {
|
|
||||||
copyString += eol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tasks.length > 0) {
|
|
||||||
let p = tasks[0]();
|
|
||||||
for (let i = 1; i < tasks.length; i++) {
|
|
||||||
p = p.then(tasks[i]);
|
|
||||||
}
|
|
||||||
p.then(() => {
|
|
||||||
this._clipboardService.writeText(copyString);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEolString(): string {
|
|
||||||
return this._textResourcePropertiesService.getEOL(URI.parse(this.uri), 'sql');
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
public getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
|
||||||
if (includeHeaders !== undefined) {
|
|
||||||
// Respect the value explicity passed into the method
|
|
||||||
return includeHeaders;
|
|
||||||
}
|
|
||||||
// else get config option from vscode config
|
|
||||||
includeHeaders = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.copyIncludeHeaders);
|
|
||||||
return !!includeHeaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldRemoveNewLines(): boolean {
|
|
||||||
// get config copyRemoveNewLine option from vscode config
|
|
||||||
let removeNewLines: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configCopyRemoveNewLine);
|
|
||||||
return !!removeNewLines;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
|
|
||||||
let headers: string[] = undefined;
|
let headers: string[] = undefined;
|
||||||
let batchSummary: azdata.BatchSummary = this._batchSets[batchId];
|
let batchSummary: azdata.BatchSummary = this._batchSets[batchId];
|
||||||
if (batchSummary !== undefined) {
|
if (batchSummary !== undefined) {
|
||||||
@@ -622,19 +556,6 @@ export default class QueryRunner extends Disposable {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeNewLines(inputString: string): string {
|
|
||||||
// This regex removes all newlines in all OS types
|
|
||||||
// Windows(CRLF): \r\n
|
|
||||||
// Linux(LF)/Modern MacOS: \n
|
|
||||||
// Old MacOs: \r
|
|
||||||
if (types.isUndefinedOrNull(inputString)) {
|
|
||||||
return 'null';
|
|
||||||
}
|
|
||||||
|
|
||||||
let outputString: string = inputString.replace(/(\r\n|\n|\r)/gm, '');
|
|
||||||
return outputString;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendBatchTimeMessage(batchId: number, executionTime: string): void {
|
private sendBatchTimeMessage(batchId: number, executionTime: string): void {
|
||||||
// get config copyRemoveNewLine option from vscode config
|
// get config copyRemoveNewLine option from vscode config
|
||||||
let showBatchTime: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configShowBatchTime);
|
let showBatchTime: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configShowBatchTime);
|
||||||
@@ -654,4 +575,80 @@ export default class QueryRunner extends Disposable {
|
|||||||
public serializeResults(batchId: number, resultSetId: number, format: SaveFormat, selection: Slick.Range[]) {
|
public serializeResults(batchId: number, resultSetId: number, format: SaveFormat, selection: Slick.Range[]) {
|
||||||
return this.instantiationService.createInstance(ResultSerializer).saveResults(this.uri, { selection, format, batchIndex: batchId, resultSetNumber: resultSetId });
|
return this.instantiationService.createInstance(ResultSerializer).saveResults(this.uri, { selection, format, batchIndex: batchId, resultSetNumber: resultSetId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getGridDataProvider(batchId: number, resultSetId: number): IGridDataProvider {
|
||||||
|
return this.instantiationService.createInstance(QueryGridDataProvider, this, batchId, resultSetId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class QueryGridDataProvider implements IGridDataProvider {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private queryRunner: QueryRunner,
|
||||||
|
private batchId: number,
|
||||||
|
private resultSetId: number,
|
||||||
|
@INotificationService private _notificationService: INotificationService,
|
||||||
|
@IClipboardService private _clipboardService: IClipboardService,
|
||||||
|
@IConfigurationService private _configurationService: IConfigurationService,
|
||||||
|
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult> {
|
||||||
|
return this.queryRunner.getQueryRows(rowStart, numberOfRows, this.batchId, this.resultSetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void {
|
||||||
|
this.copyResultsAsync(selection, includeHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
let results = await getResultsString(this, selection, includeHeaders);
|
||||||
|
this._clipboardService.writeText(results);
|
||||||
|
} catch (error) {
|
||||||
|
this._notificationService.error(nls.localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getEolString(): string {
|
||||||
|
return getEolString(this._textResourcePropertiesService, this.queryRunner.uri);
|
||||||
|
}
|
||||||
|
shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
||||||
|
return shouldIncludeHeaders(includeHeaders, this._configurationService);
|
||||||
|
}
|
||||||
|
shouldRemoveNewLines(): boolean {
|
||||||
|
return shouldRemoveNewLines(this._configurationService);
|
||||||
|
}
|
||||||
|
getColumnHeaders(range: Slick.Range): string[] {
|
||||||
|
return this.queryRunner.getColumnHeaders(this.batchId, this.resultSetId, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canSerialize(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||||
|
return this.queryRunner.serializeResults(this.batchId, this.resultSetId, format, selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getEolString(textResourcePropertiesService: ITextResourcePropertiesService, uri: string): string {
|
||||||
|
return textResourcePropertiesService.getEOL(URI.parse(uri), 'sql');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldIncludeHeaders(includeHeaders: boolean, configurationService: IConfigurationService): boolean {
|
||||||
|
if (includeHeaders !== undefined) {
|
||||||
|
// Respect the value explicity passed into the method
|
||||||
|
return includeHeaders;
|
||||||
|
}
|
||||||
|
// else get config option from vscode config
|
||||||
|
includeHeaders = WorkbenchUtils.getSqlConfigValue<boolean>(configurationService, Constants.copyIncludeHeaders);
|
||||||
|
return !!includeHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldRemoveNewLines(configurationService: IConfigurationService): boolean {
|
||||||
|
// get config copyRemoveNewLine option from vscode config
|
||||||
|
let removeNewLines: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(configurationService, Constants.configCopyRemoveNewLine);
|
||||||
|
return !!removeNewLines;
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ export class OutputComponent extends AngularDisposable implements OnInit, AfterV
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this._register(this._themeService.onThemeChange(event => this.updateTheme(event)));
|
this._register(this._themeService.onThemeChange(event => this.updateTheme(event)));
|
||||||
|
this.loadComponent();
|
||||||
this.layout();
|
this.layout();
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
this._register(Event.debounce(this.cellModel.notebookModel.layoutChanged, (l, e) => e, 50, /*leading=*/false)
|
this._register(Event.debounce(this.cellModel.notebookModel.layoutChanged, (l, e) => e, 50, /*leading=*/false)
|
||||||
@@ -62,10 +63,6 @@ export class OutputComponent extends AngularDisposable implements OnInit, AfterV
|
|||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.updateTheme(this._themeService.getTheme());
|
this.updateTheme(this._themeService.getTheme());
|
||||||
if (this.componentHost) {
|
|
||||||
this.loadComponent();
|
|
||||||
}
|
|
||||||
this._changeref.detectChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import product from 'vs/platform/product/node/product';
|
|||||||
import { registerComponentType } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
|
import { registerComponentType } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
|
||||||
import { MimeRendererComponent as MimeRendererComponent } from 'sql/workbench/parts/notebook/outputs/mimeRenderer.component';
|
import { MimeRendererComponent as MimeRendererComponent } from 'sql/workbench/parts/notebook/outputs/mimeRenderer.component';
|
||||||
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/outputs/markdownOutput.component';
|
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/outputs/markdownOutput.component';
|
||||||
|
import { GridOutputComponent } from 'sql/workbench/parts/notebook/outputs/gridOutput.component';
|
||||||
import { PlotlyOutputComponent } from 'sql/workbench/parts/notebook/outputs/plotlyOutput.component';
|
import { PlotlyOutputComponent } from 'sql/workbench/parts/notebook/outputs/plotlyOutput.component';
|
||||||
|
|
||||||
// Model View editor registration
|
// Model View editor registration
|
||||||
@@ -126,16 +127,31 @@ registerComponentType({
|
|||||||
* A mime renderer component for grid data.
|
* A mime renderer component for grid data.
|
||||||
* This will be replaced by a dedicated component in the future
|
* This will be replaced by a dedicated component in the future
|
||||||
*/
|
*/
|
||||||
registerComponentType({
|
if (product.quality !== 'stable') {
|
||||||
mimeTypes: [
|
registerComponentType({
|
||||||
'application/vnd.dataresource+json',
|
mimeTypes: [
|
||||||
'application/vnd.dataresource'
|
'application/vnd.dataresource+json',
|
||||||
],
|
'application/vnd.dataresource'
|
||||||
rank: 40,
|
],
|
||||||
safe: true,
|
rank: 40,
|
||||||
ctor: MimeRendererComponent,
|
safe: true,
|
||||||
selector: MimeRendererComponent.SELECTOR
|
ctor: GridOutputComponent,
|
||||||
});
|
selector: GridOutputComponent.SELECTOR
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Default to existing grid view until we're sure the new
|
||||||
|
// implementation is fully stable
|
||||||
|
registerComponentType({
|
||||||
|
mimeTypes: [
|
||||||
|
'application/vnd.dataresource+json',
|
||||||
|
'application/vnd.dataresource'
|
||||||
|
],
|
||||||
|
rank: 40,
|
||||||
|
safe: true,
|
||||||
|
ctor: MimeRendererComponent,
|
||||||
|
selector: MimeRendererComponent.SELECTOR
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A mime renderer component for LaTeX.
|
* A mime renderer component for LaTeX.
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import { SIDE_BAR_BACKGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, EDITOR_GROUP_H
|
|||||||
import { activeContrastBorder, contrastBorder, buttonBackground, textLinkForeground, textLinkActiveForeground, textPreformatForeground, textBlockQuoteBackground, textBlockQuoteBorder } from 'vs/platform/theme/common/colorRegistry';
|
import { activeContrastBorder, contrastBorder, buttonBackground, textLinkForeground, textLinkActiveForeground, textPreformatForeground, textBlockQuoteBackground, textBlockQuoteBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||||
import { IDisposable } from 'vscode-xterm';
|
import { IDisposable } from 'vscode-xterm';
|
||||||
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||||
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
|
import { BareResultsGridInfo, getBareResultsGridInfoStyles } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||||
|
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||||
|
import * as types from 'vs/base/common/types';
|
||||||
|
|
||||||
export function registerNotebookThemes(overrideEditorThemeSetting: boolean): IDisposable {
|
export function registerNotebookThemes(overrideEditorThemeSetting: boolean, configurationService: IConfigurationService): IDisposable {
|
||||||
return registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
|
return registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
|
||||||
|
|
||||||
let lightBoxShadow = '0px 4px 6px 0px rgba(0, 0, 0, 0.14)';
|
let lightBoxShadow = '0px 4px 6px 0px rgba(0, 0, 0, 0.14)';
|
||||||
@@ -231,5 +235,23 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean): IDi
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Results grid options. Putting these here since query editor only adds them on query editor load.
|
||||||
|
// We may want to remove from query editor as it can just live here and be loaded once, instead of once
|
||||||
|
// per editor group which is inefficient
|
||||||
|
let rawOptions = BareResultsGridInfo.createFromRawSettings(configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||||
|
|
||||||
|
let cssRuleText = '';
|
||||||
|
if (types.isNumber(rawOptions.cellPadding)) {
|
||||||
|
cssRuleText = rawOptions.cellPadding + 'px';
|
||||||
|
} else {
|
||||||
|
cssRuleText = rawOptions.cellPadding.join('px ') + 'px;';
|
||||||
|
}
|
||||||
|
collector.addRule(`.grid-panel .monaco-table .slick-cell {
|
||||||
|
padding: ${cssRuleText}
|
||||||
|
}
|
||||||
|
.grid-panel .monaco-table, .message-tree {
|
||||||
|
${getBareResultsGridInfoStyles(rawOptions)}
|
||||||
|
}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
282
src/sql/workbench/parts/notebook/outputs/gridOutput.component.ts
Normal file
282
src/sql/workbench/parts/notebook/outputs/gridOutput.component.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { OnInit, Component, Input, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
|
||||||
|
import { AngularDisposable } from 'sql/base/node/lifecycle';
|
||||||
|
import { IMimeComponent } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
|
||||||
|
import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
|
||||||
|
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
|
||||||
|
import { GridTableBase, GridTableState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
||||||
|
import { IGridDataProvider, getResultsString } from 'sql/platform/query/common/gridDataProvider';
|
||||||
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||||
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||||
|
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||||
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
|
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||||
|
import { IDataResource } from 'sql/workbench/services/notebook/sql/sqlSessionManager';
|
||||||
|
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||||
|
import { getEolString, shouldIncludeHeaders, shouldRemoveNewLines } from 'sql/platform/query/common/queryRunner';
|
||||||
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
|
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||||
|
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||||
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||||
|
import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
import { IAction } from 'vs/base/common/actions';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: GridOutputComponent.SELECTOR,
|
||||||
|
template: `<div #output class="notebook-cellTable"></div>`
|
||||||
|
})
|
||||||
|
export class GridOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
|
||||||
|
public static readonly SELECTOR: string = 'grid-output';
|
||||||
|
|
||||||
|
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
|
||||||
|
|
||||||
|
private _initialized: boolean = false;
|
||||||
|
private _cellModel: ICellModel;
|
||||||
|
private _bundleOptions: MimeModel.IOptions;
|
||||||
|
private _table: DataResourceTable;
|
||||||
|
constructor(
|
||||||
|
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||||
|
@Inject(IThemeService) private readonly themeService: IThemeService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||||
|
this._bundleOptions = value;
|
||||||
|
if (this._initialized) {
|
||||||
|
this.renderGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() mimeType: string;
|
||||||
|
|
||||||
|
get cellModel(): ICellModel {
|
||||||
|
return this._cellModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() set cellModel(value: ICellModel) {
|
||||||
|
this._cellModel = value;
|
||||||
|
if (this._initialized) {
|
||||||
|
this.renderGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.renderGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGrid(): void {
|
||||||
|
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._table) {
|
||||||
|
let source = <IDataResource><any>this._bundleOptions.data[this.mimeType];
|
||||||
|
let state = new GridTableState(0, 0);
|
||||||
|
this._table = this.instantiationService.createInstance(DataResourceTable, source, this.cellModel.notebookModel.notebookUri.toString(), state);
|
||||||
|
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||||
|
outputElement.appendChild(this._table.element);
|
||||||
|
this._register(attachTableStyler(this._table, this.themeService));
|
||||||
|
this.layout();
|
||||||
|
this._table.onAdd();
|
||||||
|
this._initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(): void {
|
||||||
|
if (this._table) {
|
||||||
|
let maxSize = Math.min(this._table.maximumSize, 500);
|
||||||
|
this._table.layout(maxSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataResourceTable extends GridTableBase<any> {
|
||||||
|
|
||||||
|
private _gridDataProvider: IGridDataProvider;
|
||||||
|
|
||||||
|
constructor(source: IDataResource,
|
||||||
|
documentUri: string,
|
||||||
|
state: GridTableState,
|
||||||
|
@IContextMenuService contextMenuService: IContextMenuService,
|
||||||
|
@IInstantiationService instantiationService: IInstantiationService,
|
||||||
|
@IEditorService editorService: IEditorService,
|
||||||
|
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||||
|
@IConfigurationService configurationService: IConfigurationService
|
||||||
|
) {
|
||||||
|
super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
||||||
|
this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, documentUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
get gridDataProvider(): IGridDataProvider {
|
||||||
|
return this._gridDataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getCurrentActions(): IAction[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getContextActions(): IAction[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public get maximumSize(): number {
|
||||||
|
// Overriding action bar size calculation for now.
|
||||||
|
// When we add this back in, we should update this calculation
|
||||||
|
return Math.max(this.maxSize, /* ACTIONBAR_HEIGHT + BOTTOM_PADDING */ 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataResourceDataProvider implements IGridDataProvider {
|
||||||
|
private rows: azdata.DbCellValue[][];
|
||||||
|
constructor(source: IDataResource,
|
||||||
|
private resultSet: azdata.ResultSetSummary,
|
||||||
|
private documentUri: string,
|
||||||
|
@INotificationService private _notificationService: INotificationService,
|
||||||
|
@IClipboardService private _clipboardService: IClipboardService,
|
||||||
|
@IConfigurationService private _configurationService: IConfigurationService,
|
||||||
|
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||||
|
) {
|
||||||
|
this.transformSource(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformSource(source: IDataResource): void {
|
||||||
|
this.rows = source.data.map(row => {
|
||||||
|
let rowData: azdata.DbCellValue[] = [];
|
||||||
|
Object.keys(row).forEach((val, index) => {
|
||||||
|
let displayValue = String(Object.values(row)[index]);
|
||||||
|
// Since the columns[0] represents the row number, start at 1
|
||||||
|
rowData.push({
|
||||||
|
displayValue: displayValue,
|
||||||
|
isNull: false,
|
||||||
|
invariantCultureDisplayValue: displayValue
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return rowData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult> {
|
||||||
|
let rowEnd = rowStart + numberOfRows;
|
||||||
|
if (rowEnd > this.rows.length) {
|
||||||
|
rowEnd = this.rows.length;
|
||||||
|
}
|
||||||
|
let resultSubset: azdata.QueryExecuteSubsetResult = {
|
||||||
|
message: undefined,
|
||||||
|
resultSubset: {
|
||||||
|
rowCount: rowEnd - rowStart,
|
||||||
|
rows: this.rows.slice(rowStart, rowEnd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Promise.resolve(resultSubset);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void {
|
||||||
|
this.copyResultsAsync(selection, includeHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
let results = await getResultsString(this, selection, includeHeaders);
|
||||||
|
this._clipboardService.writeText(results);
|
||||||
|
} catch (error) {
|
||||||
|
this._notificationService.error(localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEolString(): string {
|
||||||
|
return getEolString(this._textResourcePropertiesService, this.documentUri);
|
||||||
|
}
|
||||||
|
shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
||||||
|
return shouldIncludeHeaders(includeHeaders, this._configurationService);
|
||||||
|
}
|
||||||
|
shouldRemoveNewLines(): boolean {
|
||||||
|
return shouldRemoveNewLines(this._configurationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumnHeaders(range: Slick.Range): string[] {
|
||||||
|
let headers: string[] = this.resultSet.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
|
||||||
|
return info.columnName;
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canSerialize(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResultSet(source: IDataResource): azdata.ResultSetSummary {
|
||||||
|
let columnInfo: azdata.IDbColumn[] = source.schema.fields.map(field => {
|
||||||
|
let column = new SimpleDbColumn(field.name);
|
||||||
|
if (field.type) {
|
||||||
|
switch (field.type) {
|
||||||
|
case 'xml':
|
||||||
|
column.isXml = true;
|
||||||
|
break;
|
||||||
|
case 'json':
|
||||||
|
column.isJson = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Only handling a few cases for now
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
let summary: azdata.ResultSetSummary = {
|
||||||
|
batchId: 0,
|
||||||
|
id: 0,
|
||||||
|
complete: true,
|
||||||
|
rowCount: source.data.length,
|
||||||
|
columnInfo: columnInfo
|
||||||
|
};
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleDbColumn implements azdata.IDbColumn {
|
||||||
|
|
||||||
|
constructor(columnName: string) {
|
||||||
|
this.columnName = columnName;
|
||||||
|
}
|
||||||
|
allowDBNull?: boolean;
|
||||||
|
baseCatalogName: string;
|
||||||
|
baseColumnName: string;
|
||||||
|
baseSchemaName: string;
|
||||||
|
baseServerName: string;
|
||||||
|
baseTableName: string;
|
||||||
|
columnName: string;
|
||||||
|
columnOrdinal?: number;
|
||||||
|
columnSize?: number;
|
||||||
|
isAliased?: boolean;
|
||||||
|
isAutoIncrement?: boolean;
|
||||||
|
isExpression?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
isIdentity?: boolean;
|
||||||
|
isKey?: boolean;
|
||||||
|
isBytes?: boolean;
|
||||||
|
isChars?: boolean;
|
||||||
|
isSqlVariant?: boolean;
|
||||||
|
isUdt?: boolean;
|
||||||
|
dataType: string;
|
||||||
|
isXml?: boolean;
|
||||||
|
isJson?: boolean;
|
||||||
|
isLong?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
isUnique?: boolean;
|
||||||
|
numericPrecision?: number;
|
||||||
|
numericScale?: number;
|
||||||
|
udtAssemblyQualifiedName: string;
|
||||||
|
dataTypeName: string;
|
||||||
|
}
|
||||||
@@ -66,6 +66,9 @@ export class MarkdownOutputComponent extends AngularDisposable implements IMimeC
|
|||||||
|
|
||||||
@Input() set cellModel(value: ICellModel) {
|
@Input() set cellModel(value: ICellModel) {
|
||||||
this._cellModel = value;
|
this._cellModel = value;
|
||||||
|
if (this._initialized) {
|
||||||
|
this.updatePreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isTrusted(): boolean {
|
public get isTrusted(): boolean {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function renderDataResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SlickGrid requires columns and data to be in a very specific format; this code was adapted from tableInsight.component.ts
|
// SlickGrid requires columns and data to be in a very specific format; this code was adapted from tableInsight.component.ts
|
||||||
function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: string]: string }[] {
|
export function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: string]: string }[] {
|
||||||
return rows.map(row => {
|
return rows.map(row => {
|
||||||
let dataWithSchema = {};
|
let dataWithSchema = {};
|
||||||
Object.keys(row).forEach((val, index) => {
|
Object.keys(row).forEach((val, index) => {
|
||||||
@@ -101,7 +101,7 @@ function transformData(rows: any[], columns: Slick.Column<any>[]): { [key: strin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
export function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||||
return columns.map((col, index) => {
|
return columns.map((col, index) => {
|
||||||
return <Slick.Column<any>>{
|
return <Slick.Column<any>>{
|
||||||
name: col,
|
name: col,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
|
|||||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||||
|
|
||||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
|
||||||
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||||
import { Table } from 'sql/base/browser/ui/table/table';
|
import { Table } from 'sql/base/browser/ui/table/table';
|
||||||
import { GridTableState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
import { GridTableState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
||||||
@@ -17,16 +16,18 @@ import { QueryEditor } from './queryEditor';
|
|||||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||||
import { isWindows } from 'vs/base/common/platform';
|
import { isWindows } from 'vs/base/common/platform';
|
||||||
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
|
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
|
||||||
|
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
|
||||||
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
|
|
||||||
export interface IGridActionContext {
|
export interface IGridActionContext {
|
||||||
cell: { row: number; cell: number; };
|
gridDataProvider: IGridDataProvider;
|
||||||
selection: Slick.Range[];
|
table: Table<any>;
|
||||||
runner: QueryRunner;
|
tableState: GridTableState;
|
||||||
|
cell?: { row: number; cell: number; };
|
||||||
|
selection?: Slick.Range[];
|
||||||
|
selectionModel?: CellSelectionModel<any>;
|
||||||
batchId: number;
|
batchId: number;
|
||||||
resultId: number;
|
resultId: number;
|
||||||
table: Table<any>;
|
|
||||||
selectionModel: CellSelectionModel<any>;
|
|
||||||
tableState: GridTableState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMessagesActionContext {
|
export interface IMessagesActionContext {
|
||||||
@@ -64,19 +65,17 @@ export class SaveResultAction extends Action {
|
|||||||
label: string,
|
label: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
private format: SaveFormat,
|
private format: SaveFormat,
|
||||||
private accountForNumberColumn = true
|
@INotificationService private notificationService: INotificationService
|
||||||
) {
|
) {
|
||||||
super(id, label, icon);
|
super(id, label, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
public run(context: IGridActionContext): Promise<boolean> {
|
public async run(context: IGridActionContext): Promise<boolean> {
|
||||||
if (this.accountForNumberColumn) {
|
if (!context.gridDataProvider.canSerialize) {
|
||||||
context.runner.serializeResults(context.batchId, context.resultId, this.format,
|
this.notificationService.warn(localize('saveToFileNotSupported', "Save to file is not supported by the backing data source"));
|
||||||
mapForNumberColumn(context.selection));
|
|
||||||
} else {
|
|
||||||
context.runner.serializeResults(context.batchId, context.resultId, this.format, context.selection);
|
|
||||||
}
|
}
|
||||||
return Promise.resolve(true);
|
await context.gridDataProvider.serializeResults(this.format, mapForNumberColumn(context.selection));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +97,11 @@ export class CopyResultAction extends Action {
|
|||||||
|
|
||||||
public run(context: IGridActionContext): Promise<boolean> {
|
public run(context: IGridActionContext): Promise<boolean> {
|
||||||
if (this.accountForNumberColumn) {
|
if (this.accountForNumberColumn) {
|
||||||
context.runner.copyResults(
|
context.gridDataProvider.copyResults(
|
||||||
mapForNumberColumn(context.selection),
|
mapForNumberColumn(context.selection),
|
||||||
context.batchId, context.resultId, this.copyHeader);
|
this.copyHeader);
|
||||||
} else {
|
} else {
|
||||||
context.runner.copyResults(context.selection, context.batchId, context.resultId, this.copyHeader);
|
context.gridDataProvider.copyResults(context.selection, this.copyHeader);
|
||||||
}
|
}
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export class BareResultsGridInfo extends BareFontInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBareResultsGridInfoStyles(info: BareResultsGridInfo): string {
|
export function getBareResultsGridInfoStyles(info: BareResultsGridInfo): string {
|
||||||
let content = '';
|
let content = '';
|
||||||
if (info.fontFamily) {
|
if (info.fontFamily) {
|
||||||
content += `font-family: ${info.fontFamily};`;
|
content += `font-family: ${info.fontFamily};`;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
import QueryRunner, { QueryGridDataProvider } from 'sql/platform/query/common/queryRunner';
|
||||||
import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
|
import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
|
||||||
import { Table } from 'sql/base/browser/ui/table/table';
|
import { Table } from 'sql/base/browser/ui/table/table';
|
||||||
import { ScrollableSplitView, IView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview';
|
import { ScrollableSplitView, IView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview';
|
||||||
@@ -39,6 +39,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
|
|||||||
import { IAction } from 'vs/base/common/actions';
|
import { IAction } from 'vs/base/common/actions';
|
||||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||||
import { ILogService } from 'vs/platform/log/common/log';
|
import { ILogService } from 'vs/platform/log/common/log';
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
import { IGridDataProvider } from 'sql/platform/query/common/gridDataProvider';
|
||||||
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
||||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||||
|
|
||||||
@@ -362,7 +364,12 @@ export class GridPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridTable<T> extends Disposable implements IView {
|
export interface IDataSet {
|
||||||
|
rowCount: number;
|
||||||
|
columnInfo: azdata.IDbColumn[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class GridTableBase<T> extends Disposable implements IView {
|
||||||
private table: Table<T>;
|
private table: Table<T>;
|
||||||
private actionBar: ActionBar;
|
private actionBar: ActionBar;
|
||||||
private container = document.createElement('div');
|
private container = document.createElement('div');
|
||||||
@@ -390,24 +397,19 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
|
|
||||||
public isOnlyTable: boolean = true;
|
public isOnlyTable: boolean = true;
|
||||||
|
|
||||||
public get resultSet(): azdata.ResultSetSummary {
|
|
||||||
return this._resultSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this handles if the row count is small, like 4-5 rows
|
// this handles if the row count is small, like 4-5 rows
|
||||||
private get maxSize(): number {
|
protected get maxSize(): number {
|
||||||
return ((this.resultSet.rowCount) * this.rowHeight) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
|
return ((this.resultSet.rowCount) * this.rowHeight) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private runner: QueryRunner,
|
|
||||||
private _resultSet: azdata.ResultSetSummary,
|
|
||||||
state: GridTableState,
|
state: GridTableState,
|
||||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
protected _resultSet: azdata.ResultSetSummary,
|
||||||
@IInstantiationService private instantiationService: IInstantiationService,
|
protected contextMenuService: IContextMenuService,
|
||||||
@IEditorService private editorService: IEditorService,
|
protected instantiationService: IInstantiationService,
|
||||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
|
protected editorService: IEditorService,
|
||||||
@IConfigurationService private configurationService: IConfigurationService
|
protected untitledEditorService: IUntitledEditorService,
|
||||||
|
protected configurationService: IConfigurationService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid');
|
let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid');
|
||||||
@@ -423,7 +425,7 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
return <Slick.Column<T>>{
|
return <Slick.Column<T>>{
|
||||||
id: i.toString(),
|
id: i.toString(),
|
||||||
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
|
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
|
||||||
? 'XML Showplan'
|
? localize('xmlShowplan', "XML Showplan")
|
||||||
: escape(c.columnName),
|
: escape(c.columnName),
|
||||||
field: i.toString(),
|
field: i.toString(),
|
||||||
formatter: isLinked ? hyperLinkFormatter : textFormatter,
|
formatter: isLinked ? hyperLinkFormatter : textFormatter,
|
||||||
@@ -432,6 +434,12 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract get gridDataProvider(): IGridDataProvider;
|
||||||
|
|
||||||
|
public get resultSet(): azdata.ResultSetSummary {
|
||||||
|
return this._resultSet;
|
||||||
|
}
|
||||||
|
|
||||||
public onAdd() {
|
public onAdd() {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
let collection = new VirtualizedCollection(
|
let collection = new VirtualizedCollection(
|
||||||
@@ -505,28 +513,27 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
this.table.style(this.styles);
|
this.table.style(this.styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
let actions = this.getCurrentActions();
|
|
||||||
|
|
||||||
let actionBarContainer = document.createElement('div');
|
let actionBarContainer = document.createElement('div');
|
||||||
actionBarContainer.style.width = ACTIONBAR_WIDTH + 'px';
|
actionBarContainer.style.width = ACTIONBAR_WIDTH + 'px';
|
||||||
actionBarContainer.style.display = 'inline-block';
|
actionBarContainer.style.display = 'inline-block';
|
||||||
actionBarContainer.style.height = '100%';
|
actionBarContainer.style.height = '100%';
|
||||||
actionBarContainer.style.verticalAlign = 'top';
|
actionBarContainer.style.verticalAlign = 'top';
|
||||||
this.container.appendChild(actionBarContainer);
|
this.container.appendChild(actionBarContainer);
|
||||||
|
let context: IGridActionContext = {
|
||||||
|
gridDataProvider: this.gridDataProvider,
|
||||||
|
table: this.table,
|
||||||
|
tableState: this.state,
|
||||||
|
batchId: this.resultSet.batchId,
|
||||||
|
resultId: this.resultSet.id
|
||||||
|
};
|
||||||
this.actionBar = new ActionBar(actionBarContainer, {
|
this.actionBar = new ActionBar(actionBarContainer, {
|
||||||
orientation: ActionsOrientation.VERTICAL, context: {
|
orientation: ActionsOrientation.VERTICAL, context: context
|
||||||
runner: this.runner,
|
|
||||||
batchId: this.resultSet.batchId,
|
|
||||||
resultId: this.resultSet.id,
|
|
||||||
table: this.table,
|
|
||||||
tableState: this.state
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// update context before we run an action
|
// update context before we run an action
|
||||||
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
||||||
this.actionBar.context = this.generateContext();
|
this.actionBar.context = this.generateContext();
|
||||||
});
|
});
|
||||||
this.actionBar.push(actions, { icon: true, label: false });
|
this.rebuildActionBar();
|
||||||
|
|
||||||
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
||||||
if (this.state) {
|
if (this.state) {
|
||||||
@@ -605,7 +612,7 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
let column = this.resultSet.columnInfo[event.cell.cell - 1];
|
let column = this.resultSet.columnInfo[event.cell.cell - 1];
|
||||||
// handle if a showplan link was clicked
|
// handle if a showplan link was clicked
|
||||||
if (column && (column.isXml || column.isJson)) {
|
if (column && (column.isXml || column.isJson)) {
|
||||||
this.runner.getQueryRows(event.cell.row, 1, this.resultSet.batchId, this.resultSet.id).then(async d => {
|
this.gridDataProvider.getRowData(event.cell.row, 1).then(async d => {
|
||||||
let value = d.resultSubset.rows[0][event.cell.cell - 1];
|
let value = d.resultSubset.rows[0][event.cell.cell - 1];
|
||||||
let content = value.displayValue;
|
let content = value.displayValue;
|
||||||
|
|
||||||
@@ -631,9 +638,7 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
return <IGridActionContext>{
|
return <IGridActionContext>{
|
||||||
cell,
|
cell,
|
||||||
selection,
|
selection,
|
||||||
runner: this.runner,
|
gridDataProvider: this.gridDataProvider,
|
||||||
batchId: this.resultSet.batchId,
|
|
||||||
resultId: this.resultSet.id,
|
|
||||||
table: this.table,
|
table: this.table,
|
||||||
tableState: this.state,
|
tableState: this.state,
|
||||||
selectionModel: this.selectionModel
|
selectionModel: this.selectionModel
|
||||||
@@ -646,28 +651,9 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
this.actionBar.push(actions, { icon: true, label: false });
|
this.actionBar.push(actions, { icon: true, label: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentActions(): IAction[] {
|
protected abstract getCurrentActions(): IAction[];
|
||||||
|
|
||||||
let actions = [];
|
protected abstract getContextActions(): IAction[];
|
||||||
|
|
||||||
if (this.state.canBeMaximized) {
|
|
||||||
if (this.state.maximized) {
|
|
||||||
actions.splice(1, 0, new RestoreTableAction());
|
|
||||||
} else {
|
|
||||||
actions.splice(1, 0, new MaximizeTableAction());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actions.push(
|
|
||||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
|
||||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
|
||||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
|
||||||
new SaveResultAction(SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
|
||||||
this.instantiationService.createInstance(ChartDataAction)
|
|
||||||
);
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public layout(size?: number): void {
|
public layout(size?: number): void {
|
||||||
if (!this.table) {
|
if (!this.table) {
|
||||||
@@ -692,7 +678,7 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadData(offset: number, count: number): Thenable<T[]> {
|
private loadData(offset: number, count: number): Thenable<T[]> {
|
||||||
return this.runner.getQueryRows(offset, count, this.resultSet.batchId, this.resultSet.id).then(response => {
|
return this.gridDataProvider.getRowData(offset, count).then(response => {
|
||||||
if (!response.resultSubset) {
|
if (!response.resultSubset) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -716,17 +702,19 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
this.contextMenuService.showContextMenu({
|
this.contextMenuService.showContextMenu({
|
||||||
getAnchor: () => e.anchor,
|
getAnchor: () => e.anchor,
|
||||||
getActions: () => {
|
getActions: () => {
|
||||||
let actions = [
|
let actions: IAction[] = [
|
||||||
new SelectAllGridAction(),
|
new SelectAllGridAction(),
|
||||||
new Separator(),
|
new Separator()
|
||||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
];
|
||||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
let contributedActions: IAction[] = this.getContextActions();
|
||||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
if (contributedActions && contributedActions.length > 0) {
|
||||||
new SaveResultAction(SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
actions.push(...contributedActions);
|
||||||
new Separator(),
|
actions.push(new Separator());
|
||||||
|
}
|
||||||
|
actions.push(
|
||||||
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false),
|
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false),
|
||||||
new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true)
|
new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true)
|
||||||
];
|
);
|
||||||
|
|
||||||
if (this.state.canBeMaximized) {
|
if (this.state.canBeMaximized) {
|
||||||
if (this.state.maximized) {
|
if (this.state.maximized) {
|
||||||
@@ -787,3 +775,56 @@ class GridTable<T> extends Disposable implements IView {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GridTable<T> extends GridTableBase<T> {
|
||||||
|
private _gridDataProvider: IGridDataProvider;
|
||||||
|
constructor(
|
||||||
|
runner: QueryRunner,
|
||||||
|
resultSet: azdata.ResultSetSummary,
|
||||||
|
state: GridTableState,
|
||||||
|
@IContextMenuService contextMenuService: IContextMenuService,
|
||||||
|
@IInstantiationService instantiationService: IInstantiationService,
|
||||||
|
@IEditorService editorService: IEditorService,
|
||||||
|
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||||
|
@IConfigurationService configurationService: IConfigurationService
|
||||||
|
) {
|
||||||
|
super(state, resultSet, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
||||||
|
this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, runner, resultSet.batchId, resultSet.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
get gridDataProvider(): IGridDataProvider {
|
||||||
|
return this._gridDataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getCurrentActions(): IAction[] {
|
||||||
|
|
||||||
|
let actions = [];
|
||||||
|
|
||||||
|
if (this.state.canBeMaximized) {
|
||||||
|
if (this.state.maximized) {
|
||||||
|
actions.splice(1, 0, new RestoreTableAction());
|
||||||
|
} else {
|
||||||
|
actions.splice(1, 0, new MaximizeTableAction());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push(
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||||
|
this.instantiationService.createInstance(ChartDataAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getContextActions(): IAction[] {
|
||||||
|
return [
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||||
|
this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ export class NotebookService extends Disposable implements INotebookService {
|
|||||||
if (this._configurationService) {
|
if (this._configurationService) {
|
||||||
this.updateNotebookThemes();
|
this.updateNotebookThemes();
|
||||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||||
if (e.affectsConfiguration(OVERRIDE_EDITOR_THEMING_SETTING)) {
|
if (e.affectsConfiguration(OVERRIDE_EDITOR_THEMING_SETTING)
|
||||||
|
|| e.affectsConfiguration('resultsGrid')) {
|
||||||
this.updateNotebookThemes();
|
this.updateNotebookThemes();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -229,7 +230,7 @@ export class NotebookService extends Disposable implements INotebookService {
|
|||||||
this._themeParticipant.dispose();
|
this._themeParticipant.dispose();
|
||||||
}
|
}
|
||||||
this._overrideEditorThemeSetting = overrideEditorSetting;
|
this._overrideEditorThemeSetting = overrideEditorSetting;
|
||||||
this._themeParticipant = registerNotebookThemes(overrideEditorSetting);
|
this._themeParticipant = registerNotebookThemes(overrideEditorSetting, this._configurationService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user