mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 17:23:19 -05:00
Support Save As CSV/JSON/Excel/XML from Notebooks (#6627)
Updated existing serialization code so it actually supports serialization. Still needs work to replace the saveAs function when a QueryProvider doesn't support save as, but want to handle in separate PR. Removed separate MainThread/ExtHostSerializationProvider code as the DataProtocol code is the right place to put this code Plumbed support through the gridOutputComponent to use the new serialize method in the serialization provider Refactored the resultSerializer so majority of code can be shared between both implementations (for example file save dialog -> save -> show file on completion) * Update to latest SQLToolsService release
This commit is contained in:
@@ -29,6 +29,9 @@ import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel'
|
||||
import { GridTableState } from 'sql/workbench/parts/query/common/gridPanelState';
|
||||
import { GridTableBase } from 'sql/workbench/parts/query/browser/gridPanel';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { ISerializationService, SerializeDataParams } from 'sql/platform/serialization/common/serializationService';
|
||||
import { SaveResultAction } from 'sql/workbench/parts/query/browser/actions';
|
||||
import { ResultSerializer, SaveResultsResponse } from 'sql/workbench/parts/query/common/resultSerializer';
|
||||
|
||||
@Component({
|
||||
selector: GridOutputComponent.SELECTOR,
|
||||
@@ -110,7 +113,8 @@ class DataResourceTable extends GridTableBase<any> {
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@ISerializationService private _serializationService: ISerializationService
|
||||
) {
|
||||
super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
|
||||
this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, documentUri);
|
||||
@@ -125,7 +129,15 @@ class DataResourceTable extends GridTableBase<any> {
|
||||
}
|
||||
|
||||
protected getContextActions(): IAction[] {
|
||||
return [];
|
||||
if (!this._serializationService.hasProvider()) {
|
||||
return [];
|
||||
}
|
||||
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),
|
||||
];
|
||||
}
|
||||
|
||||
public get maximumSize(): number {
|
||||
@@ -143,7 +155,9 @@ class DataResourceDataProvider implements IGridDataProvider {
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService,
|
||||
@ISerializationService private _serializationService: ISerializationService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
this.transformSource(source);
|
||||
}
|
||||
@@ -210,14 +224,67 @@ class DataResourceDataProvider implements IGridDataProvider {
|
||||
}
|
||||
|
||||
get canSerialize(): boolean {
|
||||
return false;
|
||||
return this._serializationService.hasProvider();
|
||||
}
|
||||
|
||||
|
||||
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
let serializer = this._instantiationService.createInstance(ResultSerializer);
|
||||
return serializer.handleSerialization(this.documentUri, format, (filePath) => this.doSerialize(serializer, filePath, format, selection));
|
||||
}
|
||||
|
||||
private async doSerialize(serializer: ResultSerializer, filePath: string, format: SaveFormat, selection: Slick.Range[]): Promise<SaveResultsResponse> {
|
||||
// TODO implement selection support
|
||||
let columns = this.resultSet.columnInfo;
|
||||
let rowLength = this.rows.length;
|
||||
let minRow = 0;
|
||||
let maxRow = this.rows.length;
|
||||
let singleSelection = selection && selection.length > 0 ? selection[0] : undefined;
|
||||
if (singleSelection && this.isSelected(singleSelection)) {
|
||||
rowLength = singleSelection.toRow - singleSelection.fromRow + 1;
|
||||
minRow = singleSelection.fromRow;
|
||||
maxRow = singleSelection.toRow + 1;
|
||||
columns = columns.slice(singleSelection.fromCell, singleSelection.toCell + 1);
|
||||
}
|
||||
let getRows: ((index: number, rowCount: number) => azdata.DbCellValue[][]) = (index, rowCount) => {
|
||||
// Offset for selections by adding the selection startRow to the index
|
||||
index = index + minRow;
|
||||
if (rowLength === 0 || index < 0 || index >= maxRow) {
|
||||
return [];
|
||||
}
|
||||
let endIndex = index + rowCount;
|
||||
if (endIndex > maxRow) {
|
||||
endIndex = maxRow;
|
||||
}
|
||||
let result = this.rows.slice(index, endIndex).map(row => {
|
||||
if (this.isSelected(singleSelection)) {
|
||||
return row.slice(singleSelection.fromCell, singleSelection.toCell + 1);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
let serializeRequestParams: SerializeDataParams = <SerializeDataParams>Object.assign(serializer.getBasicSaveParameters(format), <Partial<SerializeDataParams>>{
|
||||
saveFormat: format,
|
||||
columns: columns,
|
||||
filePath: filePath,
|
||||
getRowRange: (rowStart, numberOfRows) => getRows(rowStart, numberOfRows),
|
||||
rowCount: rowLength
|
||||
});
|
||||
let result = await this._serializationService.serializeResults(serializeRequestParams);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a range of cells were selected.
|
||||
*/
|
||||
private isSelected(selection: Slick.Range): boolean {
|
||||
return (selection && !((selection.fromCell === selection.toCell) && (selection.fromRow === selection.toRow)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createResultSet(source: IDataResource): azdata.ResultSetSummary {
|
||||
let columnInfo: azdata.IDbColumn[] = source.schema.fields.map(field => {
|
||||
let column = new SimpleDbColumn(field.name);
|
||||
|
||||
@@ -23,6 +23,7 @@ import * as Constants from 'sql/workbench/contrib/extensions/common/constants';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
export interface IGridActionContext {
|
||||
gridDataProvider: IGridDataProvider;
|
||||
@@ -79,7 +80,12 @@ export class SaveResultAction extends Action {
|
||||
if (!context.gridDataProvider.canSerialize) {
|
||||
this.notificationService.warn(localize('saveToFileNotSupported', "Save to file is not supported by the backing data source"));
|
||||
}
|
||||
await context.gridDataProvider.serializeResults(this.format, mapForNumberColumn(context.selection));
|
||||
try {
|
||||
await context.gridDataProvider.serializeResults(this.format, mapForNumberColumn(context.selection));
|
||||
} catch (error) {
|
||||
this.notificationService.error(getErrorMessage(error));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
|
||||
let prevSavePath: string;
|
||||
|
||||
|
||||
export interface SaveResultsResponse {
|
||||
succeeded: boolean;
|
||||
messages: string;
|
||||
}
|
||||
|
||||
interface ICsvConfig {
|
||||
includeHeaders: boolean;
|
||||
delimiter: string;
|
||||
@@ -48,9 +54,6 @@ interface IXmlConfig {
|
||||
export class ResultSerializer {
|
||||
public static tempFileCount: number = 1;
|
||||
|
||||
private _uri: string;
|
||||
private _filePath: string;
|
||||
|
||||
constructor(
|
||||
@IOutputService private _outputService: IOutputService,
|
||||
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
||||
@@ -67,12 +70,38 @@ export class ResultSerializer {
|
||||
*/
|
||||
public saveResults(uri: string, saveRequest: ISaveRequest): Thenable<void> {
|
||||
const self = this;
|
||||
this._uri = uri;
|
||||
|
||||
// prompt for filepath
|
||||
return self.promptForFilepath(saveRequest).then(filePath => {
|
||||
return this.promptForFilepath(saveRequest.format, uri).then(filePath => {
|
||||
if (filePath) {
|
||||
return self.sendRequestToService(filePath, saveRequest.batchIndex, saveRequest.resultSetNumber, saveRequest.format, saveRequest.selection ? saveRequest.selection[0] : undefined);
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = resolveFilePath(uri, filePath, this.rootPath);
|
||||
}
|
||||
let saveResultsParams = this.getParameters(uri, filePath, saveRequest.batchIndex, saveRequest.resultSetNumber, saveRequest.format, saveRequest.selection ? saveRequest.selection[0] : undefined);
|
||||
let sendRequest = () => this.sendSaveRequestToService(saveResultsParams);
|
||||
return self.doSave(filePath, saveRequest.format, sendRequest);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
private async sendSaveRequestToService(saveResultsParams: SaveResultsRequestParams): Promise<SaveResultsResponse> {
|
||||
let result = await this._queryManagementService.saveResults(saveResultsParams);
|
||||
return {
|
||||
succeeded: !result.messages,
|
||||
messages: result.messages
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save request by getting filename from user and sending request to service
|
||||
*/
|
||||
public handleSerialization(uri: string, format: SaveFormat, sendRequest: ((filePath: string) => Promise<SaveResultsResponse>)): Thenable<void> {
|
||||
const self = this;
|
||||
return this.promptForFilepath(format, uri).then(filePath => {
|
||||
if (filePath) {
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = resolveFilePath(uri, filePath, this.rootPath);
|
||||
}
|
||||
return self.doSave(filePath, format, () => sendRequest(filePath));
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
@@ -100,25 +129,26 @@ export class ResultSerializer {
|
||||
this.outputChannel.append(message);
|
||||
}
|
||||
|
||||
private promptForFilepath(saveRequest: ISaveRequest): Thenable<string> {
|
||||
let filepathPlaceHolder = prevSavePath ? path.dirname(prevSavePath) : resolveCurrentDirectory(this._uri, this.rootPath);
|
||||
|
||||
private promptForFilepath(format: SaveFormat, resourceUri: string): Thenable<string> {
|
||||
let filepathPlaceHolder = prevSavePath ? path.dirname(prevSavePath) : resolveCurrentDirectory(resourceUri, this.rootPath);
|
||||
if (filepathPlaceHolder) {
|
||||
filepathPlaceHolder = path.join(filepathPlaceHolder, this.getResultsDefaultFilename(saveRequest));
|
||||
filepathPlaceHolder = path.join(filepathPlaceHolder, this.getResultsDefaultFilename(format));
|
||||
}
|
||||
|
||||
return this.fileDialogService.showSaveDialog({
|
||||
title: nls.localize('resultsSerializer.saveAsFileTitle', "Choose Results File"),
|
||||
defaultUri: filepathPlaceHolder ? URI.file(filepathPlaceHolder) : undefined,
|
||||
filters: this.getResultsFileExtension(saveRequest)
|
||||
filters: this.getResultsFileExtension(format)
|
||||
}).then(filePath => {
|
||||
prevSavePath = filePath.fsPath;
|
||||
return filePath.fsPath;
|
||||
});
|
||||
}
|
||||
|
||||
private getResultsDefaultFilename(saveRequest: ISaveRequest): string {
|
||||
private getResultsDefaultFilename(format: SaveFormat): string {
|
||||
let fileName = 'Results';
|
||||
switch (saveRequest.format) {
|
||||
switch (format) {
|
||||
case SaveFormat.CSV:
|
||||
fileName = fileName + '.csv';
|
||||
break;
|
||||
@@ -137,11 +167,11 @@ export class ResultSerializer {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private getResultsFileExtension(saveRequest: ISaveRequest): FileFilter[] {
|
||||
private getResultsFileExtension(format: SaveFormat): FileFilter[] {
|
||||
let fileFilters = new Array<FileFilter>();
|
||||
let fileFilter: { extensions: string[]; name: string } = { extensions: undefined, name: undefined };
|
||||
|
||||
switch (saveRequest.format) {
|
||||
switch (format) {
|
||||
case SaveFormat.CSV:
|
||||
fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionCSVTitle', "CSV (Comma delimited)");
|
||||
fileFilter.extensions = ['csv'];
|
||||
@@ -167,6 +197,22 @@ export class ResultSerializer {
|
||||
return fileFilters;
|
||||
}
|
||||
|
||||
public getBasicSaveParameters(format: string): SaveResultsRequestParams {
|
||||
let saveResultsParams: SaveResultsRequestParams;
|
||||
|
||||
if (format === SaveFormat.CSV) {
|
||||
saveResultsParams = this.getConfigForCsv();
|
||||
} else if (format === SaveFormat.JSON) {
|
||||
saveResultsParams = this.getConfigForJson();
|
||||
} else if (format === SaveFormat.EXCEL) {
|
||||
saveResultsParams = this.getConfigForExcel();
|
||||
} else if (format === SaveFormat.XML) {
|
||||
saveResultsParams = this.getConfigForXml();
|
||||
}
|
||||
return saveResultsParams;
|
||||
}
|
||||
|
||||
|
||||
private getConfigForCsv(): SaveResultsRequestParams {
|
||||
let saveResultsParams = <SaveResultsRequestParams>{ resultFormat: SaveFormat.CSV as string };
|
||||
|
||||
@@ -231,26 +277,11 @@ export class ResultSerializer {
|
||||
return saveResultsParams;
|
||||
}
|
||||
|
||||
private getParameters(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): SaveResultsRequestParams {
|
||||
let saveResultsParams: SaveResultsRequestParams;
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
this._filePath = resolveFilePath(this._uri, filePath, this.rootPath);
|
||||
} else {
|
||||
this._filePath = filePath;
|
||||
}
|
||||
|
||||
if (format === SaveFormat.CSV) {
|
||||
saveResultsParams = this.getConfigForCsv();
|
||||
} else if (format === SaveFormat.JSON) {
|
||||
saveResultsParams = this.getConfigForJson();
|
||||
} else if (format === SaveFormat.EXCEL) {
|
||||
saveResultsParams = this.getConfigForExcel();
|
||||
} else if (format === SaveFormat.XML) {
|
||||
saveResultsParams = this.getConfigForXml();
|
||||
}
|
||||
|
||||
saveResultsParams.filePath = this._filePath;
|
||||
saveResultsParams.ownerUri = this._uri;
|
||||
private getParameters(uri: string, filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): SaveResultsRequestParams {
|
||||
let saveResultsParams = this.getBasicSaveParameters(format);
|
||||
saveResultsParams.filePath = filePath;
|
||||
saveResultsParams.ownerUri = uri;
|
||||
saveResultsParams.resultSetIndex = resultSetNo;
|
||||
saveResultsParams.batchIndex = batchIndex;
|
||||
if (this.isSelected(selection)) {
|
||||
@@ -297,13 +328,13 @@ export class ResultSerializer {
|
||||
/**
|
||||
* Send request to sql tools service to save a result set
|
||||
*/
|
||||
private sendRequestToService(filePath: string, batchIndex: number, resultSetNo: number, format: string, selection: Slick.Range): Thenable<void> {
|
||||
let saveResultsParams = this.getParameters(filePath, batchIndex, resultSetNo, format, selection);
|
||||
private async doSave(filePath: string, format: string, sendRequest: () => Promise<SaveResultsResponse>): Promise<void> {
|
||||
|
||||
this.logToOutputChannel(LocalizedConstants.msgSaveStarted + this._filePath);
|
||||
this.logToOutputChannel(LocalizedConstants.msgSaveStarted + filePath);
|
||||
|
||||
// send message to the sqlserverclient for converting results to the requested format and saving to filepath
|
||||
return this._queryManagementService.saveResults(saveResultsParams).then(result => {
|
||||
try {
|
||||
let result = await sendRequest();
|
||||
if (result.messages) {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
@@ -311,20 +342,20 @@ export class ResultSerializer {
|
||||
});
|
||||
this.logToOutputChannel(LocalizedConstants.msgSaveFailed + result.messages);
|
||||
} else {
|
||||
this.promptFileSavedNotification(this._filePath);
|
||||
this.promptFileSavedNotification(filePath);
|
||||
this.logToOutputChannel(LocalizedConstants.msgSaveSucceeded + filePath);
|
||||
this.openSavedFile(this._filePath, format);
|
||||
this.openSavedFile(filePath, format);
|
||||
}
|
||||
// TODO telemetry for save results
|
||||
// Telemetry.sendTelemetryEvent('SavedResults', { 'type': format });
|
||||
|
||||
}, error => {
|
||||
} catch (error) {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: LocalizedConstants.msgSaveFailed + error
|
||||
});
|
||||
this.logToOutputChannel(LocalizedConstants.msgSaveFailed + error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user