@@ -9,7 +9,7 @@ import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
|
||||
import { QueryEditor } from 'sql/workbench/parts/query/browser/queryEditor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IQueryModelService, IQueryEvent } from 'sql/platform/query/common/queryModel';
|
||||
|
||||
@@ -8,8 +8,8 @@ import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiat
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { QueryResultsInput } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { IQueryEditorOptions } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { QueryPlanInput } from 'sql/workbench/parts/queryPlan/common/queryPlanInput';
|
||||
import { NotebookInput } from 'sql/workbench/parts/notebook/notebookInput';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import ConnectionConstants = require('sql/platform/connection/common/constants');
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
@@ -19,7 +19,7 @@ import { IInsightsConfig } from 'sql/workbench/parts/dashboard/widgets/insights/
|
||||
import { IInsightsDialogService } from 'sql/workbench/services/insights/common/insightsDialogService';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { DashboardInput } from 'sql/workbench/parts/dashboard/dashboardInput';
|
||||
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
|
||||
213
src/sql/workbench/parts/charts/browser/actions.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsightOptions, IInsight } from './interfaces';
|
||||
import { Graph } from './graphInsight';
|
||||
import { QueryEditor } from 'sql/workbench/parts/query/browser/queryEditor';
|
||||
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
|
||||
import { IInsightsConfig } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { resolveCurrentDirectory, getRootPath } from 'sql/platform/node/pathUtilities';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { join, normalize } from 'vs/base/common/path';
|
||||
import { writeFile } from 'vs/base/node/pfs';
|
||||
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export interface IChartActionContext {
|
||||
options: IInsightOptions;
|
||||
insight: IInsight;
|
||||
}
|
||||
|
||||
export class CreateInsightAction extends Action {
|
||||
public static ID = 'chartview.createInsight';
|
||||
public static LABEL = localize('createInsightLabel', "Create Insight");
|
||||
public static ICON = 'createInsight';
|
||||
|
||||
constructor(
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService
|
||||
) {
|
||||
super(CreateInsightAction.ID, CreateInsightAction.LABEL, CreateInsightAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IChartActionContext): Promise<boolean> {
|
||||
let uriString: string = this.getActiveUriString();
|
||||
if (!uriString) {
|
||||
this.showError(localize('createInsightNoEditor', 'Cannot create insight as the active editor is not a SQL Editor'));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
let uri: URI = URI.parse(uriString);
|
||||
let queryFile: string = uri.fsPath;
|
||||
let query: string = undefined;
|
||||
let type = {};
|
||||
let options = Object.assign({}, context.options);
|
||||
delete options.type;
|
||||
type[context.options.type] = options;
|
||||
// create JSON
|
||||
let config: IInsightsConfig = {
|
||||
type,
|
||||
query,
|
||||
queryFile
|
||||
};
|
||||
|
||||
let widgetConfig = {
|
||||
name: localize('myWidgetName', 'My-Widget'),
|
||||
gridItemConfig: {
|
||||
sizex: 2,
|
||||
sizey: 1
|
||||
},
|
||||
widget: {
|
||||
'insights-widget': config
|
||||
}
|
||||
};
|
||||
|
||||
let input = this.untitledEditorService.createOrGet(undefined, 'json', JSON.stringify(widgetConfig));
|
||||
|
||||
return this.editorService.openEditor(input, { pinned: true })
|
||||
.then(
|
||||
() => true,
|
||||
error => {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: error
|
||||
});
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getActiveUriString(): string {
|
||||
let editor = this.editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
return queryEditor.uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private showError(errorMsg: string) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyAction extends Action {
|
||||
public static ID = 'chartview.copy';
|
||||
public static LABEL = localize('copyChartLabel', "Copy as image");
|
||||
public static ICON = 'copyImage';
|
||||
|
||||
constructor(
|
||||
@IClipboardService private clipboardService: IClipboardService,
|
||||
@INotificationService private notificationService: INotificationService
|
||||
) {
|
||||
super(CopyAction.ID, CopyAction.LABEL, CopyAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IChartActionContext): Promise<boolean> {
|
||||
if (context.insight instanceof Graph) {
|
||||
let data = context.insight.getCanvasData();
|
||||
if (!data) {
|
||||
this.showError(localize('chartNotFound', 'Could not find chart to save'));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
this.clipboardService.writeImageDataUrl(data);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
private showError(errorMsg: string) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveImageAction extends Action {
|
||||
public static ID = 'chartview.saveImage';
|
||||
public static LABEL = localize('saveImageLabel', "Save as image");
|
||||
public static ICON = 'saveAsImage';
|
||||
|
||||
constructor(
|
||||
@IWindowsService private windowsService: IWindowsService,
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService
|
||||
) {
|
||||
super(SaveImageAction.ID, SaveImageAction.LABEL, SaveImageAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IChartActionContext): Promise<boolean> {
|
||||
if (context.insight instanceof Graph) {
|
||||
return this.promptForFilepath().then(filePath => {
|
||||
let data = (<Graph>context.insight).getCanvasData();
|
||||
if (!data) {
|
||||
this.showError(localize('chartNotFound', 'Could not find chart to save'));
|
||||
return false;
|
||||
}
|
||||
if (filePath) {
|
||||
let buffer = this.decodeBase64Image(data);
|
||||
writeFile(filePath, buffer).then(undefined, (err) => {
|
||||
if (err) {
|
||||
this.showError(err.message);
|
||||
} else {
|
||||
let fileUri = URI.file(filePath);
|
||||
this.windowsService.openExternal(fileUri.toString());
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: localize('chartSaved', 'Saved Chart to path: {0}', filePath)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
private decodeBase64Image(data: string): Buffer {
|
||||
let matches = data.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
||||
return Buffer.from(matches[2], 'base64');
|
||||
}
|
||||
|
||||
private promptForFilepath(): Promise<string> {
|
||||
let filepathPlaceHolder = resolveCurrentDirectory(this.getActiveUriString(), getRootPath(this.workspaceContextService));
|
||||
filepathPlaceHolder = join(filepathPlaceHolder, 'chart.png');
|
||||
return this.windowService.showSaveDialog({
|
||||
title: localize('chartViewer.saveAsFileTitle', 'Choose Results File'),
|
||||
defaultPath: normalize(filepathPlaceHolder)
|
||||
});
|
||||
}
|
||||
|
||||
private showError(errorMsg: string) {
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
|
||||
private getActiveUriString(): string {
|
||||
let editor = this.editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
return queryEditor.uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
220
src/sql/workbench/parts/charts/browser/chartOptions.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { InsightType, IInsightOptions } from './interfaces';
|
||||
import { DataDirection, ChartType, LegendPosition, DataType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
export enum ControlType {
|
||||
combo,
|
||||
numberInput,
|
||||
input,
|
||||
checkbox,
|
||||
dateInput
|
||||
}
|
||||
|
||||
export interface IChartOption {
|
||||
label: string;
|
||||
type: ControlType;
|
||||
configEntry: string;
|
||||
default: any;
|
||||
options?: any[];
|
||||
displayableOptions?: string[];
|
||||
if?: (options: IInsightOptions) => boolean;
|
||||
}
|
||||
|
||||
export interface IChartOptions {
|
||||
general: Array<IChartOption>;
|
||||
[x: string]: Array<IChartOption>;
|
||||
}
|
||||
|
||||
const dataDirectionOption: IChartOption = {
|
||||
label: localize('dataDirectionLabel', 'Data Direction'),
|
||||
type: ControlType.combo,
|
||||
displayableOptions: [localize('verticalLabel', 'Vertical'), localize('horizontalLabel', 'Horizontal')],
|
||||
options: [DataDirection.Vertical, DataDirection.Horizontal],
|
||||
configEntry: 'dataDirection',
|
||||
default: DataDirection.Horizontal
|
||||
};
|
||||
|
||||
const columnsAsLabelsInput: IChartOption = {
|
||||
label: localize('columnsAsLabelsLabel', 'Use column names as labels'),
|
||||
type: ControlType.checkbox,
|
||||
configEntry: 'columnsAsLabels',
|
||||
default: false,
|
||||
if: (options: IInsightOptions) => {
|
||||
return options.dataDirection === DataDirection.Vertical && options.dataType !== DataType.Point;
|
||||
}
|
||||
};
|
||||
|
||||
const labelFirstColumnInput: IChartOption = {
|
||||
label: localize('labelFirstColumnLabel', 'Use first column as row label'),
|
||||
type: ControlType.checkbox,
|
||||
configEntry: 'labelFirstColumn',
|
||||
default: false,
|
||||
if: (options: IInsightOptions) => {
|
||||
return options.dataDirection === DataDirection.Horizontal && options.dataType !== DataType.Point;
|
||||
}
|
||||
};
|
||||
|
||||
const legendInput: IChartOption = {
|
||||
label: localize('legendLabel', 'Legend Position'),
|
||||
type: ControlType.combo,
|
||||
options: Object.values(LegendPosition),
|
||||
configEntry: 'legendPosition',
|
||||
default: LegendPosition.Top
|
||||
};
|
||||
|
||||
const yAxisLabelInput: IChartOption = {
|
||||
label: localize('yAxisLabel', 'Y Axis Label'),
|
||||
type: ControlType.input,
|
||||
configEntry: 'yAxisLabel',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const yAxisMinInput: IChartOption = {
|
||||
label: localize('yAxisMinVal', 'Y Axis Minimum Value'),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'yAxisMin',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const yAxisMaxInput: IChartOption = {
|
||||
label: localize('yAxisMaxVal', 'Y Axis Maximum Value'),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'yAxisMax',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisLabelInput: IChartOption = {
|
||||
label: localize('xAxisLabel', 'X Axis Label'),
|
||||
type: ControlType.input,
|
||||
configEntry: 'xAxisLabel',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMinInput: IChartOption = {
|
||||
label: localize('xAxisMinVal', 'X Axis Minimum Value'),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'xAxisMin',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMaxInput: IChartOption = {
|
||||
label: localize('xAxisMaxVal', 'X Axis Maximum Value'),
|
||||
type: ControlType.numberInput,
|
||||
configEntry: 'xAxisMax',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMinDateInput: IChartOption = {
|
||||
label: localize('xAxisMinDate', 'X Axis Minimum Date'),
|
||||
type: ControlType.dateInput,
|
||||
configEntry: 'xAxisMin',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const xAxisMaxDateInput: IChartOption = {
|
||||
label: localize('xAxisMaxDate', 'X Axis Maximum Date'),
|
||||
type: ControlType.dateInput,
|
||||
configEntry: 'xAxisMax',
|
||||
default: undefined
|
||||
};
|
||||
|
||||
const dataTypeInput: IChartOption = {
|
||||
label: localize('dataTypeLabel', 'Data Type'),
|
||||
type: ControlType.combo,
|
||||
options: [DataType.Number, DataType.Point],
|
||||
displayableOptions: [localize('numberLabel', 'Number'), localize('pointLabel', 'Point')],
|
||||
configEntry: 'dataType',
|
||||
default: DataType.Number
|
||||
};
|
||||
|
||||
export const ChartOptions: IChartOptions = {
|
||||
general: [
|
||||
{
|
||||
label: localize('chartTypeLabel', 'Chart Type'),
|
||||
type: ControlType.combo,
|
||||
options: insightRegistry.getAllIds(),
|
||||
configEntry: 'type',
|
||||
default: ChartType.Bar
|
||||
}
|
||||
],
|
||||
[ChartType.Line]: [
|
||||
dataTypeInput,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
yAxisLabelInput,
|
||||
xAxisLabelInput,
|
||||
legendInput
|
||||
],
|
||||
[ChartType.Scatter]: [
|
||||
legendInput,
|
||||
yAxisLabelInput,
|
||||
xAxisLabelInput
|
||||
],
|
||||
[ChartType.TimeSeries]: [
|
||||
legendInput,
|
||||
yAxisLabelInput,
|
||||
yAxisMinInput,
|
||||
yAxisMaxInput,
|
||||
xAxisLabelInput,
|
||||
xAxisMinDateInput,
|
||||
xAxisMaxDateInput,
|
||||
],
|
||||
[ChartType.Bar]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput,
|
||||
yAxisLabelInput,
|
||||
yAxisMinInput,
|
||||
yAxisMaxInput,
|
||||
xAxisLabelInput
|
||||
],
|
||||
[ChartType.HorizontalBar]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput,
|
||||
xAxisLabelInput,
|
||||
xAxisMinInput,
|
||||
xAxisMaxInput,
|
||||
yAxisLabelInput
|
||||
],
|
||||
[ChartType.Pie]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput
|
||||
],
|
||||
[ChartType.Doughnut]: [
|
||||
dataDirectionOption,
|
||||
columnsAsLabelsInput,
|
||||
labelFirstColumnInput,
|
||||
legendInput
|
||||
],
|
||||
[InsightType.Table]: [],
|
||||
[InsightType.Count]: [],
|
||||
[InsightType.Image]: [
|
||||
{
|
||||
configEntry: 'encoding',
|
||||
label: localize('encodingOption', 'Encoding'),
|
||||
type: ControlType.input,
|
||||
default: 'hex'
|
||||
},
|
||||
{
|
||||
configEntry: 'imageFormat',
|
||||
label: localize('imageFormatOption', 'Image Format'),
|
||||
type: ControlType.input,
|
||||
default: 'jpeg'
|
||||
}
|
||||
]
|
||||
};
|
||||
37
src/sql/workbench/parts/charts/browser/chartTab.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IPanelTab } from 'sql/base/browser/ui/panel/panel';
|
||||
import { ChartView } from './chartView';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class ChartTab implements IPanelTab {
|
||||
public readonly title = localize('chartTabTitle', 'Chart');
|
||||
public readonly identifier = 'ChartTab';
|
||||
public readonly view: ChartView;
|
||||
|
||||
constructor(@IInstantiationService instantiationService: IInstantiationService) {
|
||||
this.view = instantiationService.createInstance(ChartView);
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this.view.queryRunner = runner;
|
||||
}
|
||||
|
||||
public chart(dataId: { batchId: number, resultId: number }): void {
|
||||
this.view.chart(dataId);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.view.dispose();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.view.clear();
|
||||
}
|
||||
}
|
||||
384
src/sql/workbench/parts/charts/browser/chartView.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/chartView';
|
||||
|
||||
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
|
||||
import { Insight } from './insight';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { ChartOptions, IChartOption, ControlType } from './chartOptions';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { IInsightOptions } from './interfaces';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { attachSelectBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { ChartType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { CreateInsightAction, CopyAction, SaveImageAction, IChartActionContext } from 'sql/workbench/parts/charts/browser/actions';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
|
||||
export class ChartState {
|
||||
dataId: { batchId: number, resultId: number };
|
||||
options: IInsightOptions = {
|
||||
type: ChartType.Bar
|
||||
};
|
||||
|
||||
dispose() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
declare class Proxy {
|
||||
constructor(object, handler);
|
||||
}
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
export class ChartView extends Disposable implements IPanelView {
|
||||
private insight: Insight;
|
||||
private _queryRunner: QueryRunner;
|
||||
private _data: IInsightData;
|
||||
private _currentData: { batchId: number, resultId: number };
|
||||
private taskbar: Taskbar;
|
||||
|
||||
private _createInsightAction: CreateInsightAction;
|
||||
private _copyAction: CopyAction;
|
||||
private _saveAction: SaveImageAction;
|
||||
|
||||
private _state: ChartState;
|
||||
|
||||
private options: IInsightOptions = {
|
||||
type: ChartType.Bar
|
||||
};
|
||||
|
||||
/** parent container */
|
||||
private container: HTMLElement;
|
||||
/** container for the options controls */
|
||||
private optionsControl: HTMLElement;
|
||||
/** container for type specific controls */
|
||||
private typeControls: HTMLElement;
|
||||
/** container for the insight */
|
||||
private insightContainer: HTMLElement;
|
||||
/** container for the action bar */
|
||||
private taskbarContainer: HTMLElement;
|
||||
/** container for the charting (includes insight and options) */
|
||||
private chartingContainer: HTMLElement;
|
||||
|
||||
private optionDisposables: IDisposable[] = [];
|
||||
private optionMap: { [x: string]: { element: HTMLElement; set: (val) => void } } = {};
|
||||
|
||||
constructor(
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IThemeService private _themeService: IThemeService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this.taskbarContainer = DOM.$('div.taskbar-container');
|
||||
this.taskbar = new Taskbar(this.taskbarContainer);
|
||||
this.optionsControl = DOM.$('div.options-container');
|
||||
const generalControls = DOM.$('div.general-controls');
|
||||
this.optionsControl.appendChild(generalControls);
|
||||
this.typeControls = DOM.$('div.type-controls');
|
||||
this.optionsControl.appendChild(this.typeControls);
|
||||
|
||||
this._createInsightAction = this._instantiationService.createInstance(CreateInsightAction);
|
||||
this._copyAction = this._instantiationService.createInstance(CopyAction);
|
||||
this._saveAction = this._instantiationService.createInstance(SaveImageAction);
|
||||
|
||||
this.taskbar.setContent([{ action: this._createInsightAction }]);
|
||||
|
||||
const self = this;
|
||||
this.options = new Proxy(this.options, {
|
||||
get: function (target, key, receiver) {
|
||||
return Reflect.get(target, key, receiver);
|
||||
},
|
||||
set: function (target, key, value, receiver) {
|
||||
let change = false;
|
||||
if (target[key] !== value) {
|
||||
change = true;
|
||||
}
|
||||
|
||||
let result = Reflect.set(target, key, value, receiver);
|
||||
// mirror the change in our state
|
||||
if (self.state) {
|
||||
Reflect.set(self.state.options, key, value);
|
||||
}
|
||||
|
||||
if (change) {
|
||||
self.taskbar.context = <IChartActionContext>{ options: self.options, insight: self.insight ? self.insight.insight : undefined };
|
||||
if (key === 'type') {
|
||||
self.buildOptions();
|
||||
} else {
|
||||
self.verifyOptions();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}) as IInsightOptions;
|
||||
|
||||
ChartOptions.general[0].options = insightRegistry.getAllIds();
|
||||
ChartOptions.general.map(o => {
|
||||
this.createOption(o, generalControls);
|
||||
});
|
||||
this.buildOptions();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.optionDisposables);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
if (!this.container) {
|
||||
this.container = DOM.$('div.chart-parent-container');
|
||||
this.insightContainer = DOM.$('div.insight-container');
|
||||
this.chartingContainer = DOM.$('div.charting-container');
|
||||
this.container.appendChild(this.taskbarContainer);
|
||||
this.container.appendChild(this.chartingContainer);
|
||||
this.chartingContainer.appendChild(this.insightContainer);
|
||||
this.chartingContainer.appendChild(this.optionsControl);
|
||||
this.insight = new Insight(this.insightContainer, this.options, this._instantiationService);
|
||||
}
|
||||
|
||||
container.appendChild(this.container);
|
||||
|
||||
if (this._data) {
|
||||
this.insight.data = this._data;
|
||||
} else {
|
||||
this.queryRunner = this._queryRunner;
|
||||
}
|
||||
this.verifyOptions();
|
||||
}
|
||||
|
||||
public chart(dataId: { batchId: number, resultId: number }) {
|
||||
this.state.dataId = dataId;
|
||||
this._currentData = dataId;
|
||||
this.shouldGraph();
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
if (this.insight) {
|
||||
this.insight.layout(new DOM.Dimension(DOM.getContentWidth(this.insightContainer), DOM.getContentHeight(this.insightContainer)));
|
||||
}
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this._queryRunner = runner;
|
||||
this.shouldGraph();
|
||||
}
|
||||
|
||||
private shouldGraph() {
|
||||
// Check if we have the necessary information
|
||||
if (this._currentData && this._queryRunner) {
|
||||
// check if we are being asked to graph something that is available
|
||||
let batch = this._queryRunner.batchSets[this._currentData.batchId];
|
||||
if (batch) {
|
||||
let summary = batch.resultSetSummaries[this._currentData.resultId];
|
||||
if (summary) {
|
||||
this._queryRunner.getQueryRows(0, summary.rowCount, 0, 0).then(d => {
|
||||
this._data = {
|
||||
columns: summary.columnInfo.map(c => c.columnName),
|
||||
rows: d.resultSubset.rows.map(r => r.map(c => c.displayValue))
|
||||
};
|
||||
if (this.insight) {
|
||||
this.insight.data = this._data;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// if we have the necessary information but the information isn't available yet,
|
||||
// we should be smart and retrying when the information might be available
|
||||
}
|
||||
}
|
||||
|
||||
private buildOptions() {
|
||||
// The first element in the disposables list is for the chart type: the master dropdown that controls other option controls.
|
||||
// whiling rebuilding the options we should not dispose it, otherwise it would react to the theme change event
|
||||
if (this.optionDisposables.length > 1) {
|
||||
dispose(this.optionDisposables.slice(1));
|
||||
this.optionDisposables.splice(1);
|
||||
}
|
||||
|
||||
this.optionMap = {
|
||||
'type': this.optionMap['type']
|
||||
};
|
||||
DOM.clearNode(this.typeControls);
|
||||
|
||||
this.updateActionbar();
|
||||
ChartOptions[this.options.type].map(o => {
|
||||
this.createOption(o, this.typeControls);
|
||||
});
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
this.verifyOptions();
|
||||
}
|
||||
|
||||
private verifyOptions() {
|
||||
this.updateActionbar();
|
||||
for (let key in this.optionMap) {
|
||||
if (this.optionMap.hasOwnProperty(key)) {
|
||||
let option = ChartOptions[this.options.type].find(e => e.configEntry === key);
|
||||
if (option && option.if) {
|
||||
if (option.if(this.options)) {
|
||||
DOM.show(this.optionMap[key].element);
|
||||
} else {
|
||||
DOM.hide(this.optionMap[key].element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateActionbar() {
|
||||
if (this.insight && this.insight.isCopyable) {
|
||||
this.taskbar.context = { insight: this.insight.insight, options: this.options };
|
||||
this.taskbar.setContent([
|
||||
{ action: this._createInsightAction },
|
||||
{ action: this._copyAction },
|
||||
{ action: this._saveAction }
|
||||
]);
|
||||
} else {
|
||||
this.taskbar.setContent([{ action: this._createInsightAction }]);
|
||||
}
|
||||
}
|
||||
|
||||
private createOption(option: IChartOption, container: HTMLElement) {
|
||||
let label = DOM.$('div');
|
||||
label.innerText = option.label;
|
||||
let optionContainer = DOM.$('div.option-container');
|
||||
optionContainer.appendChild(label);
|
||||
let setFunc: (val) => void;
|
||||
let value = this.state ? this.state.options[option.configEntry] || option.default : option.default;
|
||||
switch (option.type) {
|
||||
case ControlType.checkbox:
|
||||
let checkbox = new Checkbox(optionContainer, {
|
||||
label: '',
|
||||
ariaLabel: option.label,
|
||||
checked: value,
|
||||
onChange: () => {
|
||||
if (this.options[option.configEntry] !== checkbox.checked) {
|
||||
this.options[option.configEntry] = checkbox.checked;
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: boolean) => {
|
||||
checkbox.checked = val;
|
||||
};
|
||||
break;
|
||||
case ControlType.combo:
|
||||
let dropdown = new SelectBox(option.displayableOptions || option.options, undefined, this._contextViewService);
|
||||
dropdown.select(option.options.indexOf(value));
|
||||
dropdown.render(optionContainer);
|
||||
dropdown.onDidSelect(e => {
|
||||
if (this.options[option.configEntry] !== option.options[e.index]) {
|
||||
this.options[option.configEntry] = option.options[e.index];
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
dropdown.select(option.options.indexOf(val));
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachSelectBoxStyler(dropdown, this._themeService));
|
||||
break;
|
||||
case ControlType.input:
|
||||
let input = new InputBox(optionContainer, this._contextViewService);
|
||||
input.value = value || '';
|
||||
input.onDidChange(e => {
|
||||
if (this.options[option.configEntry] !== e) {
|
||||
this.options[option.configEntry] = e;
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
input.value = val;
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachInputBoxStyler(input, this._themeService));
|
||||
break;
|
||||
case ControlType.numberInput:
|
||||
let numberInput = new InputBox(optionContainer, this._contextViewService, { type: 'number' });
|
||||
numberInput.value = value || '';
|
||||
numberInput.onDidChange(e => {
|
||||
if (this.options[option.configEntry] !== Number(e)) {
|
||||
this.options[option.configEntry] = Number(e);
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
numberInput.value = val;
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachInputBoxStyler(numberInput, this._themeService));
|
||||
break;
|
||||
case ControlType.dateInput:
|
||||
let dateInput = new InputBox(optionContainer, this._contextViewService, { type: 'datetime-local' });
|
||||
dateInput.value = value || '';
|
||||
dateInput.onDidChange(e => {
|
||||
if (this.options[option.configEntry] !== e) {
|
||||
this.options[option.configEntry] = e;
|
||||
if (this.insight) {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
});
|
||||
setFunc = (val: string) => {
|
||||
if (!isUndefinedOrNull(val)) {
|
||||
dateInput.value = val;
|
||||
}
|
||||
};
|
||||
this.optionDisposables.push(attachInputBoxStyler(dateInput, this._themeService));
|
||||
break;
|
||||
}
|
||||
this.optionMap[option.configEntry] = { element: optionContainer, set: setFunc };
|
||||
container.appendChild(optionContainer);
|
||||
this.options[option.configEntry] = value;
|
||||
}
|
||||
|
||||
public set state(val: ChartState) {
|
||||
this._state = val;
|
||||
if (this.state.options) {
|
||||
for (let key in this.state.options) {
|
||||
if (this.state.options.hasOwnProperty(key) && this.optionMap[key]) {
|
||||
this.options[key] = this.state.options[key];
|
||||
this.optionMap[key].set(this.state.options[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.state.dataId) {
|
||||
this.chart(this.state.dataId);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): ChartState {
|
||||
return this._state;
|
||||
}
|
||||
}
|
||||
45
src/sql/workbench/parts/charts/browser/countInsight.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/countInsight';
|
||||
|
||||
import { IInsight, InsightType } from './interfaces';
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
|
||||
import { $, clearNode } from 'vs/base/browser/dom';
|
||||
|
||||
export class CountInsight implements IInsight {
|
||||
public options;
|
||||
public static readonly types = [InsightType.Count];
|
||||
public readonly types = CountInsight.types;
|
||||
|
||||
private countImage: HTMLElement;
|
||||
|
||||
constructor(container: HTMLElement, options: any) {
|
||||
this.countImage = $('div');
|
||||
container.appendChild(this.countImage);
|
||||
}
|
||||
|
||||
public layout() { }
|
||||
|
||||
set data(data: IInsightData) {
|
||||
clearNode(this.countImage);
|
||||
|
||||
for (let i = 0; i < data.columns.length; i++) {
|
||||
let container = $('div.count-label-container');
|
||||
let label = $('span.label-container');
|
||||
label.innerText = data.columns[i];
|
||||
let value = $('span.value-container');
|
||||
value.innerText = data.rows[0][i];
|
||||
container.appendChild(label);
|
||||
container.appendChild(value);
|
||||
this.countImage.appendChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
}
|
||||
}
|
||||
440
src/sql/workbench/parts/charts/browser/graphInsight.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Chart as ChartJs } from 'chart.js';
|
||||
|
||||
import { mixin } from 'sql/base/common/objects';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as colors from 'vs/platform/theme/common/colorRegistry';
|
||||
import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { IInsightOptions, IInsight } from './interfaces';
|
||||
import { ChartType, DataDirection, LegendPosition, DataType, IPointDataSet, customMixin } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
const noneLineGraphs = [ChartType.Doughnut, ChartType.Pie];
|
||||
|
||||
const timeSeriesScales: ChartJs.ChartOptions = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
display: true,
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
}],
|
||||
|
||||
yAxes: [{
|
||||
display: true,
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const defaultOptions: IInsightOptions = {
|
||||
type: ChartType.Bar,
|
||||
dataDirection: DataDirection.Horizontal
|
||||
};
|
||||
|
||||
export class Graph implements IInsight {
|
||||
private _options: IInsightOptions;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private chartjs: ChartJs;
|
||||
private _data: IInsightData;
|
||||
|
||||
private originalType: ChartType;
|
||||
|
||||
public static readonly types = [ChartType.Bar, ChartType.Doughnut, ChartType.HorizontalBar, ChartType.Line, ChartType.Pie, ChartType.Scatter, ChartType.TimeSeries];
|
||||
public readonly types = Graph.types;
|
||||
|
||||
private _theme: ITheme;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement, options: IInsightOptions = defaultOptions,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
this._theme = themeService.getTheme();
|
||||
themeService.onThemeChange(e => {
|
||||
this._theme = e;
|
||||
this.data = this._data;
|
||||
});
|
||||
this.options = mixin(options, defaultOptions, false);
|
||||
|
||||
let canvasContainer = document.createElement('div');
|
||||
canvasContainer.style.width = '100%';
|
||||
canvasContainer.style.height = '100%';
|
||||
|
||||
this.canvas = document.createElement('canvas');
|
||||
canvasContainer.appendChild(this.canvas);
|
||||
|
||||
container.appendChild(canvasContainer);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
|
||||
public getCanvasData(): string {
|
||||
return this.chartjs.toBase64Image();
|
||||
}
|
||||
|
||||
public set data(data: IInsightData) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
let labels: Array<string>;
|
||||
let chartData: Array<ChartJs.ChartDataSets>;
|
||||
|
||||
if (this.options.dataDirection === DataDirection.Horizontal) {
|
||||
if (this.options.labelFirstColumn) {
|
||||
labels = data.columns.slice(1);
|
||||
} else {
|
||||
labels = data.columns;
|
||||
}
|
||||
} else {
|
||||
labels = data.rows.map(row => row[0]);
|
||||
}
|
||||
|
||||
if (this.originalType === ChartType.TimeSeries) {
|
||||
let dataSetMap: { [label: string]: IPointDataSet } = {};
|
||||
this._data.rows.map(row => {
|
||||
if (row && row.length >= 3) {
|
||||
let legend = row[0];
|
||||
if (!dataSetMap[legend]) {
|
||||
dataSetMap[legend] = { label: legend, data: [], fill: false };
|
||||
}
|
||||
dataSetMap[legend].data.push({ x: row[1], y: Number(row[2]) });
|
||||
}
|
||||
});
|
||||
chartData = Object.values(dataSetMap);
|
||||
} else {
|
||||
if (this.options.dataDirection === DataDirection.Horizontal) {
|
||||
if (this.options.labelFirstColumn) {
|
||||
chartData = data.rows.map((row) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)).slice(1),
|
||||
label: row[0]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
chartData = data.rows.map((row, i) => {
|
||||
return {
|
||||
data: row.map(item => Number(item)),
|
||||
label: localize('series', 'Series {0}', i)
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.options.columnsAsLabels) {
|
||||
chartData = data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: data.rows.map(row => Number(row[i + 1])),
|
||||
label: data.columns[i + 1]
|
||||
};
|
||||
});
|
||||
} else {
|
||||
chartData = data.rows[0].slice(1).map((row, i) => {
|
||||
return {
|
||||
data: data.rows.map(row => Number(row[i + 1])),
|
||||
label: localize('series', 'Series {0}', i + 1)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chartData = chartData.map((c, i) => {
|
||||
return mixin(c, getColors(this.options.type, i, c.data.length), false);
|
||||
});
|
||||
|
||||
if (this.chartjs) {
|
||||
this.chartjs.data.datasets = chartData;
|
||||
this.chartjs.config.type = this.options.type;
|
||||
// we don't want to include lables for timeSeries
|
||||
this.chartjs.data.labels = this.originalType === 'timeSeries' ? [] : labels;
|
||||
this.chartjs.options = this.transformOptions(this.options);
|
||||
this.chartjs.update(0);
|
||||
} else {
|
||||
this.chartjs = new ChartJs(this.canvas.getContext('2d'), {
|
||||
data: {
|
||||
// we don't want to include lables for timeSeries
|
||||
labels: this.originalType === 'timeSeries' ? [] : labels,
|
||||
datasets: chartData
|
||||
},
|
||||
type: this.options.type,
|
||||
options: this.transformOptions(this.options)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private transformOptions(options: IInsightOptions): ChartJs.ChartOptions {
|
||||
let retval: ChartJs.ChartOptions = {};
|
||||
retval.maintainAspectRatio = false;
|
||||
|
||||
let foregroundColor = this._theme.getColor(colors.editorForeground);
|
||||
let foreground = foregroundColor ? foregroundColor.toString() : null;
|
||||
let gridLinesColor = this._theme.getColor(editorLineNumbers);
|
||||
let gridLines = gridLinesColor ? gridLinesColor.toString() : null;
|
||||
let backgroundColor = this._theme.getColor(colors.editorBackground);
|
||||
let background = backgroundColor ? backgroundColor.toString() : null;
|
||||
|
||||
if (options) {
|
||||
retval.scales = {};
|
||||
// we only want to include axis if it is a axis based graph type
|
||||
if (!noneLineGraphs.includes(options.type as ChartType)) {
|
||||
retval.scales.xAxes = [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground,
|
||||
labelString: options.xAxisLabel,
|
||||
display: options.xAxisLabel ? true : false
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}];
|
||||
|
||||
if (options.xAxisMax) {
|
||||
retval.scales = mixin(retval.scales, { xAxes: [{ ticks: { max: options.xAxisMax } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
if (options.xAxisMin) {
|
||||
retval.scales = mixin(retval.scales, { xAxes: [{ ticks: { min: options.xAxisMin } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
retval.scales.yAxes = [{
|
||||
scaleLabel: {
|
||||
fontColor: foreground,
|
||||
labelString: options.yAxisLabel,
|
||||
display: options.yAxisLabel ? true : false
|
||||
},
|
||||
ticks: {
|
||||
fontColor: foreground
|
||||
},
|
||||
gridLines: {
|
||||
color: gridLines
|
||||
}
|
||||
}];
|
||||
|
||||
if (options.yAxisMax) {
|
||||
retval.scales = mixin(retval.scales, { yAxes: [{ ticks: { max: options.yAxisMax } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
if (options.yAxisMin) {
|
||||
retval.scales = mixin(retval.scales, { yAxes: [{ ticks: { min: options.yAxisMin } }] }, true, customMixin);
|
||||
}
|
||||
|
||||
if (this.originalType === ChartType.TimeSeries) {
|
||||
retval = mixin(retval, timeSeriesScales, true, customMixin);
|
||||
if (options.xAxisMax) {
|
||||
retval = mixin(retval, {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
time: {
|
||||
max: options.xAxisMax
|
||||
}
|
||||
}],
|
||||
}
|
||||
}, true, customMixin);
|
||||
}
|
||||
|
||||
if (options.xAxisMin) {
|
||||
retval = mixin(retval, {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
time: {
|
||||
min: options.xAxisMin
|
||||
}
|
||||
}],
|
||||
}
|
||||
}, true, customMixin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retval.legend = <ChartJs.ChartLegendOptions>{
|
||||
position: options.legendPosition as ChartJs.PositionType,
|
||||
display: options.legendPosition !== LegendPosition.None,
|
||||
labels: {
|
||||
fontColor: foreground
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// these are custom options that will throw compile errors
|
||||
(<any>retval).viewArea = {
|
||||
backgroundColor: background
|
||||
};
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
public set options(options: IInsightOptions) {
|
||||
this._options = options;
|
||||
this.originalType = options.type as ChartType;
|
||||
if (this.options.type === ChartType.TimeSeries) {
|
||||
this.options.type = ChartType.Line;
|
||||
this.options.dataType = DataType.Point;
|
||||
this.options.dataDirection = DataDirection.Horizontal;
|
||||
}
|
||||
this.data = this._data;
|
||||
}
|
||||
|
||||
public get options(): IInsightOptions {
|
||||
return this._options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Following code is pulled from ng2-charting in order to keep the same
|
||||
* color functionality
|
||||
*/
|
||||
|
||||
const defaultColors: Array<Color> = [
|
||||
[255, 99, 132],
|
||||
[54, 162, 235],
|
||||
[255, 206, 86],
|
||||
[231, 233, 237],
|
||||
[75, 192, 192],
|
||||
[151, 187, 205],
|
||||
[220, 220, 220],
|
||||
[247, 70, 74],
|
||||
[70, 191, 189],
|
||||
[253, 180, 92],
|
||||
[148, 159, 177],
|
||||
[77, 83, 96]
|
||||
];
|
||||
|
||||
type Color = [number, number, number];
|
||||
|
||||
interface ILineColor {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
pointBackgroundColor: string;
|
||||
pointBorderColor: string;
|
||||
pointHoverBackgroundColor: string;
|
||||
pointHoverBorderColor: string;
|
||||
}
|
||||
|
||||
interface IBarColor {
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
hoverBackgroundColor: string;
|
||||
hoverBorderColor: string;
|
||||
}
|
||||
|
||||
interface IPieColors {
|
||||
backgroundColor: Array<string>;
|
||||
borderColor: Array<string>;
|
||||
pointBackgroundColor: Array<string>;
|
||||
pointBorderColor: Array<string>;
|
||||
pointHoverBackgroundColor: Array<string>;
|
||||
pointHoverBorderColor: Array<string>;
|
||||
}
|
||||
|
||||
interface IPolarAreaColors {
|
||||
backgroundColor: Array<string>;
|
||||
borderColor: Array<string>;
|
||||
hoverBackgroundColor: Array<string>;
|
||||
hoverBorderColor: Array<string>;
|
||||
}
|
||||
|
||||
function rgba(colour: Color, alpha: number): string {
|
||||
return 'rgba(' + colour.concat(alpha).join(',') + ')';
|
||||
}
|
||||
|
||||
function getRandomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function getRandomColor(): Color {
|
||||
return [getRandomInt(0, 255), getRandomInt(0, 255), getRandomInt(0, 255)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors for line|bar charts
|
||||
*/
|
||||
function generateColor(index: number): Color {
|
||||
return defaultColors[index] || getRandomColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors for pie|doughnut charts
|
||||
*/
|
||||
function generateColors(count: number): Array<Color> {
|
||||
const colorsArr = new Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
colorsArr[i] = defaultColors[i] || getRandomColor();
|
||||
}
|
||||
return colorsArr;
|
||||
}
|
||||
|
||||
function formatLineColor(colors: Color): ILineColor {
|
||||
return {
|
||||
backgroundColor: rgba(colors, 0.4),
|
||||
borderColor: rgba(colors, 1),
|
||||
pointBackgroundColor: rgba(colors, 1),
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: rgba(colors, 0.8)
|
||||
};
|
||||
}
|
||||
|
||||
function formatBarColor(colors: Color): IBarColor {
|
||||
return {
|
||||
backgroundColor: rgba(colors, 0.6),
|
||||
borderColor: rgba(colors, 1),
|
||||
hoverBackgroundColor: rgba(colors, 0.8),
|
||||
hoverBorderColor: rgba(colors, 1)
|
||||
};
|
||||
}
|
||||
|
||||
function formatPieColors(colors: Array<Color>): IPieColors {
|
||||
return {
|
||||
backgroundColor: colors.map(color => rgba(color, 0.6)),
|
||||
borderColor: colors.map(() => '#fff'),
|
||||
pointBackgroundColor: colors.map(color => rgba(color, 1)),
|
||||
pointBorderColor: colors.map(() => '#fff'),
|
||||
pointHoverBackgroundColor: colors.map(color => rgba(color, 1)),
|
||||
pointHoverBorderColor: colors.map(color => rgba(color, 1))
|
||||
};
|
||||
}
|
||||
|
||||
function formatPolarAreaColors(colors: Array<Color>): IPolarAreaColors {
|
||||
return {
|
||||
backgroundColor: colors.map(color => rgba(color, 0.6)),
|
||||
borderColor: colors.map(color => rgba(color, 1)),
|
||||
hoverBackgroundColor: colors.map(color => rgba(color, 0.8)),
|
||||
hoverBorderColor: colors.map(color => rgba(color, 1))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors by chart type
|
||||
*/
|
||||
function getColors(chartType: string, index: number, count: number): Color | ILineColor | IBarColor | IPieColors | IPolarAreaColors {
|
||||
if (chartType === 'pie' || chartType === 'doughnut') {
|
||||
return formatPieColors(generateColors(count));
|
||||
}
|
||||
if (chartType === 'polarArea') {
|
||||
return formatPolarAreaColors(generateColors(count));
|
||||
}
|
||||
if (chartType === 'line' || chartType === 'radar') {
|
||||
return formatLineColor(generateColor(index));
|
||||
}
|
||||
if (chartType === 'bar' || chartType === 'horizontalBar') {
|
||||
return formatBarColor(generateColor(index));
|
||||
}
|
||||
return generateColor(index);
|
||||
}
|
||||
72
src/sql/workbench/parts/charts/browser/imageInsight.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInsight, IInsightOptions, InsightType } from './interfaces';
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
|
||||
export interface IConfig extends IInsightOptions {
|
||||
encoding?: string;
|
||||
imageFormat?: string;
|
||||
}
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
type: InsightType.Image,
|
||||
encoding: 'hex',
|
||||
imageFormat: 'jpeg'
|
||||
};
|
||||
|
||||
export class ImageInsight implements IInsight {
|
||||
|
||||
public static readonly types = [InsightType.Image];
|
||||
public readonly types = ImageInsight.types;
|
||||
|
||||
private _options: IConfig;
|
||||
|
||||
private imageEle: HTMLImageElement;
|
||||
|
||||
constructor(container: HTMLElement, options: IConfig) {
|
||||
this._options = mixin(options, defaultConfig, false);
|
||||
this.imageEle = $('img');
|
||||
container.appendChild(this.imageEle);
|
||||
}
|
||||
|
||||
public layout() {
|
||||
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
||||
}
|
||||
|
||||
set options(config: IConfig) {
|
||||
this._options = mixin(config, defaultConfig, false);
|
||||
}
|
||||
|
||||
get options(): IConfig {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
set data(data: IInsightData) {
|
||||
if (data.rows && data.rows.length > 0 && data.rows[0].length > 0) {
|
||||
let img = data.rows[0][0];
|
||||
if (this._options.encoding === 'hex') {
|
||||
img = ImageInsight._hexToBase64(img);
|
||||
}
|
||||
this.imageEle.src = `data:image/${this._options.imageFormat};base64,${img}`;
|
||||
}
|
||||
}
|
||||
|
||||
private static _hexToBase64(hexVal: string) {
|
||||
|
||||
if (hexVal.startsWith('0x')) {
|
||||
hexVal = hexVal.slice(2);
|
||||
}
|
||||
// should be able to be replaced with new Buffer(hexVal, 'hex').toString('base64')
|
||||
return btoa(String.fromCharCode.apply(null, hexVal.replace(/\r|\n/g, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ').map(v => Number(v))));
|
||||
}
|
||||
}
|
||||
105
src/sql/workbench/parts/charts/browser/insight.ts
Normal 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Graph } from './graphInsight';
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { DataDirection, ChartType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
import { ImageInsight } from './imageInsight';
|
||||
import { TableInsight } from './tableInsight';
|
||||
import { IInsightOptions, IInsight, InsightType, IInsightCtor } from './interfaces';
|
||||
import { CountInsight } from './countInsight';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Dimension, clearNode } from 'vs/base/browser/dom';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
|
||||
const defaultOptions: IInsightOptions = {
|
||||
type: ChartType.Bar,
|
||||
dataDirection: DataDirection.Horizontal
|
||||
};
|
||||
|
||||
export class Insight {
|
||||
private _insight: IInsight;
|
||||
|
||||
public get insight(): IInsight {
|
||||
return this._insight;
|
||||
}
|
||||
|
||||
private _options: IInsightOptions;
|
||||
private _data: IInsightData;
|
||||
private dim: Dimension;
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement, options: IInsightOptions = defaultOptions,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService
|
||||
) {
|
||||
this.options = options;
|
||||
this.buildInsight();
|
||||
}
|
||||
|
||||
public layout(dim: Dimension) {
|
||||
this.dim = dim;
|
||||
this.insight.layout(dim);
|
||||
}
|
||||
|
||||
public set options(val: IInsightOptions) {
|
||||
this._options = deepClone(val);
|
||||
if (this.insight) {
|
||||
// check to see if we need to change the insight type
|
||||
if (!this.insight.types.includes(this.options.type)) {
|
||||
this.buildInsight();
|
||||
} else {
|
||||
this.insight.options = this.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get options(): IInsightOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
public set data(val: IInsightData) {
|
||||
this._data = val;
|
||||
if (this.insight) {
|
||||
this.insight.data = val;
|
||||
}
|
||||
}
|
||||
|
||||
private buildInsight() {
|
||||
if (this.insight) {
|
||||
this.insight.dispose();
|
||||
}
|
||||
|
||||
clearNode(this.container);
|
||||
|
||||
let ctor = this.findctor(this.options.type);
|
||||
|
||||
if (ctor) {
|
||||
this._insight = this._instantiationService.createInstance(ctor, this.container, this.options);
|
||||
this.insight.layout(this.dim);
|
||||
if (this._data) {
|
||||
this.insight.data = this._data;
|
||||
}
|
||||
}
|
||||
}
|
||||
public get isCopyable(): boolean {
|
||||
return Graph.types.includes(this.options.type as ChartType);
|
||||
}
|
||||
|
||||
private findctor(type: ChartType | InsightType): IInsightCtor {
|
||||
if (Graph.types.includes(type as ChartType)) {
|
||||
return Graph;
|
||||
} else if (ImageInsight.types.includes(type as InsightType)) {
|
||||
return ImageInsight;
|
||||
} else if (TableInsight.types.includes(type as InsightType)) {
|
||||
return TableInsight;
|
||||
} else if (CountInsight.types.includes(type as InsightType)) {
|
||||
return CountInsight;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
45
src/sql/workbench/parts/charts/browser/interfaces.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { DataDirection, ChartType, LegendPosition, DataType } from 'sql/workbench/parts/dashboard/widgets/insights/views/charts/interfaces';
|
||||
|
||||
export interface IInsightOptions {
|
||||
type: InsightType | ChartType;
|
||||
dataDirection?: DataDirection;
|
||||
dataType?: DataType;
|
||||
labelFirstColumn?: boolean;
|
||||
columnsAsLabels?: boolean;
|
||||
legendPosition?: LegendPosition;
|
||||
yAxisLabel?: string;
|
||||
yAxisMin?: number;
|
||||
yAxisMax?: number;
|
||||
xAxisLabel?: string;
|
||||
xAxisMin?: number;
|
||||
xAxisMax?: number;
|
||||
encoding?: string;
|
||||
imageFormat?: string;
|
||||
}
|
||||
|
||||
export interface IInsight {
|
||||
options: IInsightOptions;
|
||||
data: IInsightData;
|
||||
readonly types: Array<InsightType | ChartType>;
|
||||
layout(dim: Dimension);
|
||||
dispose();
|
||||
}
|
||||
|
||||
export interface IInsightCtor {
|
||||
new(container: HTMLElement, options: IInsightOptions, ...services: { _serviceBrand: any; }[]): IInsight;
|
||||
readonly types: Array<InsightType | ChartType>;
|
||||
}
|
||||
|
||||
export enum InsightType {
|
||||
Image = 'image',
|
||||
Table = 'table',
|
||||
Count = 'count'
|
||||
}
|
||||
30
src/sql/workbench/parts/charts/browser/media/chartView.css
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.chart-parent-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actionbar-container {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.charting-container {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.insight-container {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
width: 250px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.count-label-container {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.label-container {
|
||||
font-size: 20px
|
||||
}
|
||||
74
src/sql/workbench/parts/charts/browser/tableInsight.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IInsight, InsightType } from './interfaces';
|
||||
import { IInsightData } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
|
||||
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
|
||||
import { $, Dimension } from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class TableInsight extends Disposable implements IInsight {
|
||||
public static readonly types = [InsightType.Table];
|
||||
public readonly types = TableInsight.types;
|
||||
|
||||
private table: Table<any>;
|
||||
private dataView: TableDataView<any>;
|
||||
private columns: Slick.Column<any>[];
|
||||
|
||||
constructor(container: HTMLElement, options: any,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
let tableContainer = $('div');
|
||||
tableContainer.style.width = '100%';
|
||||
tableContainer.style.height = '100%';
|
||||
container.appendChild(tableContainer);
|
||||
this.dataView = new TableDataView();
|
||||
this.table = new Table(tableContainer, { dataProvider: this.dataView }, { showRowNumber: true });
|
||||
this.table.setSelectionModel(new CellSelectionModel());
|
||||
this._register(attachTableStyler(this.table, themeService));
|
||||
}
|
||||
|
||||
set data(data: IInsightData) {
|
||||
this.dataView.clear();
|
||||
this.dataView.push(transformData(data.rows, data.columns));
|
||||
this.columns = transformColumns(data.columns);
|
||||
this.table.columns = this.columns;
|
||||
}
|
||||
|
||||
layout(dim: Dimension) {
|
||||
this.table.layout(dim);
|
||||
}
|
||||
|
||||
public options;
|
||||
|
||||
}
|
||||
|
||||
function transformData(rows: string[][], columns: string[]): { [key: string]: string }[] {
|
||||
return rows.map(row => {
|
||||
let object: { [key: string]: string } = {};
|
||||
row.forEach((val, index) => {
|
||||
object[columns[index]] = val;
|
||||
});
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
function transformColumns(columns: string[]): Slick.Column<any>[] {
|
||||
return columns.map(col => {
|
||||
return <Slick.Column<any>>{
|
||||
name: col,
|
||||
id: col,
|
||||
field: col
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { INotificationService, INotificationActions } from 'vs/platform/notifica
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
|
||||
import { DashboardInput } from 'sql/workbench/parts/dashboard/dashboardInput';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/parts/query/editor/media/queryEditor';
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as nls from 'vs/nls';
|
||||
@@ -19,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
|
||||
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import * as queryContext from 'sql/parts/query/common/queryContext';
|
||||
import * as queryContext from 'sql/workbench/parts/query/common/queryContext';
|
||||
import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
@@ -32,7 +30,7 @@ import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResour
|
||||
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/parts/query/views/flexibleSash';
|
||||
import { IFlexibleSash, HorizontalFlexibleSash } from 'sql/workbench/parts/query/browser/flexibleSash';
|
||||
import { EditDataResultsEditor } from 'sql/workbench/parts/editData/browser/editDataResultsEditor';
|
||||
import { EditDataResultsInput } from 'sql/workbench/parts/editData/common/editDataResultsInput';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as types from 'vs/base/common/types';
|
||||
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import { bootstrapAngular } from 'sql/platform/bootstrap/node/bootstrapService';
|
||||
import { BareResultsGridInfo } from 'sql/parts/query/editor/queryResultsEditor';
|
||||
import { BareResultsGridInfo } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||
import { IEditDataComponentParams } from 'sql/platform/bootstrap/node/bootstrapParams';
|
||||
import { EditDataModule } from 'sql/workbench/parts/grid/views/editData/editData.module';
|
||||
import { EDITDATA_SELECTOR } from 'sql/workbench/parts/grid/views/editData/editData.component';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as GridContentEvents from 'sql/workbench/parts/grid/common/gridContentEvents';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
|
||||
import { QueryEditor } from 'sql/workbench/parts/query/browser/queryEditor';
|
||||
import { EditDataEditor } from 'sql/workbench/parts/editData/browser/editDataEditor';
|
||||
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
@@ -10,15 +10,15 @@ import { Subscription, Subject } from 'rxjs/Rx';
|
||||
import { ElementRef, QueryList, ChangeDetectorRef, ViewChildren } from '@angular/core';
|
||||
import { SlickGrid } from 'angular2-slickgrid';
|
||||
import { toDisposableSubscription } from 'sql/base/node/rxjsUtils';
|
||||
import * as Constants from 'sql/parts/query/common/constants';
|
||||
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
|
||||
import * as Constants from 'sql/workbench/parts/query/common/constants';
|
||||
import * as LocalizedConstants from 'sql/workbench/parts/query/common/localizedConstants';
|
||||
import { IGridInfo, IGridDataSet, SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||
import * as Utils from 'sql/platform/connection/common/utils';
|
||||
import { DataService } from 'sql/workbench/parts/grid/services/dataService';
|
||||
import * as actions from 'sql/workbench/parts/grid/views/gridActions';
|
||||
import * as Services from 'sql/base/browser/ui/table/formatters';
|
||||
import * as GridContentEvents from 'sql/workbench/parts/grid/common/gridContentEvents';
|
||||
import { ResultsVisibleContext, ResultsGridFocussedContext, ResultsMessagesFocussedContext, QueryEditorVisibleContext } from 'sql/parts/query/common/queryContext';
|
||||
import { ResultsVisibleContext, ResultsGridFocussedContext, ResultsMessagesFocussedContext, QueryEditorVisibleContext } from 'sql/workbench/parts/query/common/queryContext';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
|
||||
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
||||
import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin';
|
||||
import { RESULTS_GRID_DEFAULTS } from 'sql/parts/query/editor/queryResultsEditor';
|
||||
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||
|
||||
/**
|
||||
* Render DataResource as a grid into a host node.
|
||||
|
||||
216
src/sql/workbench/parts/query/browser/actions.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { ITree } from 'vs/base/parts/tree/browser/tree';
|
||||
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 { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { GridTableState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
||||
import { QueryEditor } from './queryEditor';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
|
||||
|
||||
export interface IGridActionContext {
|
||||
cell: { row: number; cell: number; };
|
||||
selection: Slick.Range[];
|
||||
runner: QueryRunner;
|
||||
batchId: number;
|
||||
resultId: number;
|
||||
table: Table<any>;
|
||||
selectionModel: CellSelectionModel<any>;
|
||||
tableState: GridTableState;
|
||||
}
|
||||
|
||||
export interface IMessagesActionContext {
|
||||
selection: Selection;
|
||||
tree: ITree;
|
||||
}
|
||||
|
||||
function mapForNumberColumn(ranges: Slick.Range[]): Slick.Range[] {
|
||||
if (ranges) {
|
||||
return ranges.map(e => new Slick.Range(e.fromRow, e.fromCell - 1, e.toRow, e.toCell ? e.toCell - 1 : undefined));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveResultAction extends Action {
|
||||
public static SAVECSV_ID = 'grid.saveAsCsv';
|
||||
public static SAVECSV_LABEL = localize('saveAsCsv', 'Save As CSV');
|
||||
public static SAVECSV_ICON = 'saveCsv';
|
||||
|
||||
public static SAVEJSON_ID = 'grid.saveAsJson';
|
||||
public static SAVEJSON_LABEL = localize('saveAsJson', 'Save As JSON');
|
||||
public static SAVEJSON_ICON = 'saveJson';
|
||||
|
||||
public static SAVEEXCEL_ID = 'grid.saveAsExcel';
|
||||
public static SAVEEXCEL_LABEL = localize('saveAsExcel', 'Save As Excel');
|
||||
public static SAVEEXCEL_ICON = 'saveExcel';
|
||||
|
||||
public static SAVEXML_ID = 'grid.saveAsXml';
|
||||
public static SAVEXML_LABEL = localize('saveAsXml', 'Save As XML');
|
||||
public static SAVEXML_ICON = 'saveXml';
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
icon: string,
|
||||
private format: SaveFormat,
|
||||
private accountForNumberColumn = true
|
||||
) {
|
||||
super(id, label, icon);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
if (this.accountForNumberColumn) {
|
||||
context.runner.serializeResults(context.batchId, context.resultId, this.format,
|
||||
mapForNumberColumn(context.selection));
|
||||
} else {
|
||||
context.runner.serializeResults(context.batchId, context.resultId, this.format, context.selection);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyResultAction extends Action {
|
||||
public static COPY_ID = 'grid.copySelection';
|
||||
public static COPY_LABEL = localize('copySelection', 'Copy');
|
||||
|
||||
public static COPYWITHHEADERS_ID = 'grid.copyWithHeaders';
|
||||
public static COPYWITHHEADERS_LABEL = localize('copyWithHeaders', 'Copy With Headers');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private copyHeader: boolean,
|
||||
private accountForNumberColumn = true
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
if (this.accountForNumberColumn) {
|
||||
context.runner.copyResults(
|
||||
mapForNumberColumn(context.selection),
|
||||
context.batchId, context.resultId, this.copyHeader);
|
||||
} else {
|
||||
context.runner.copyResults(context.selection, context.batchId, context.resultId, this.copyHeader);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectAllGridAction extends Action {
|
||||
public static ID = 'grid.selectAll';
|
||||
public static LABEL = localize('selectAll', 'Select All');
|
||||
|
||||
constructor() {
|
||||
super(SelectAllGridAction.ID, SelectAllGridAction.LABEL);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
context.selectionModel.setSelectedRanges([new Slick.Range(0, 0, context.table.getData().getLength() - 1, context.table.columns.length - 1)]);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyMessagesAction extends Action {
|
||||
public static ID = 'grid.messages.copy';
|
||||
public static LABEL = localize('copyMessages', 'Copy');
|
||||
|
||||
constructor(
|
||||
@IClipboardService private clipboardService: IClipboardService
|
||||
) {
|
||||
super(CopyMessagesAction.ID, CopyMessagesAction.LABEL);
|
||||
}
|
||||
|
||||
public run(context: IMessagesActionContext): Promise<boolean> {
|
||||
this.clipboardService.writeText(context.selection.toString());
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
const lineDelimiter = isWindows ? '\r\n' : '\n';
|
||||
export class CopyAllMessagesAction extends Action {
|
||||
public static ID = 'grid.messages.copyAll';
|
||||
public static LABEL = localize('copyAll', "Copy All");
|
||||
|
||||
constructor(
|
||||
private tree: ITree,
|
||||
@IClipboardService private clipboardService: IClipboardService) {
|
||||
super(CopyAllMessagesAction.ID, CopyAllMessagesAction.LABEL);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
let text = '';
|
||||
const navigator = this.tree.getNavigator();
|
||||
// skip first navigator element - the root node
|
||||
while (navigator.next()) {
|
||||
if (text) {
|
||||
text += lineDelimiter;
|
||||
}
|
||||
text += (navigator.current()).message;
|
||||
}
|
||||
|
||||
this.clipboardService.writeText(removeAnsiEscapeCodes(text));
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class MaximizeTableAction extends Action {
|
||||
public static ID = 'grid.maximize';
|
||||
public static LABEL = localize('maximize', 'Maximize');
|
||||
public static ICON = 'extendFullScreen';
|
||||
|
||||
constructor() {
|
||||
super(MaximizeTableAction.ID, MaximizeTableAction.LABEL, MaximizeTableAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
context.tableState.maximized = true;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class RestoreTableAction extends Action {
|
||||
public static ID = 'grid.restore';
|
||||
public static LABEL = localize('restore', 'Restore');
|
||||
public static ICON = 'exitFullScreen';
|
||||
|
||||
constructor() {
|
||||
super(RestoreTableAction.ID, RestoreTableAction.LABEL, RestoreTableAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
context.tableState.maximized = false;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChartDataAction extends Action {
|
||||
public static ID = 'grid.chart';
|
||||
public static LABEL = localize('chart', 'Chart');
|
||||
public static ICON = 'viewChart';
|
||||
|
||||
constructor(@IEditorService private editorService: IEditorService) {
|
||||
super(ChartDataAction.ID, ChartDataAction.LABEL, ChartDataAction.ICON);
|
||||
}
|
||||
|
||||
public run(context: IGridActionContext): Promise<boolean> {
|
||||
let activeEditor = this.editorService.activeControl;
|
||||
if (activeEditor instanceof QueryEditor) {
|
||||
activeEditor.resultsEditor.chart({ batchId: context.batchId, resultId: context.resultId });
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
220
src/sql/workbench/parts/query/browser/flavorStatus.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/flavorStatus';
|
||||
import { $, append, show, hide } from 'vs/base/browser/dom';
|
||||
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import errors = require('vs/base/common/errors');
|
||||
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
|
||||
import { DidChangeLanguageFlavorParams } from 'azdata';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
|
||||
export interface ISqlProviderEntry extends IQuickPickItem {
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
// Query execution status
|
||||
class SqlProviderEntry implements ISqlProviderEntry {
|
||||
constructor(public providerId: string, private _providerDisplayName?: string) {
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
// If display name is provided, use it. Else use default
|
||||
if (this._providerDisplayName) {
|
||||
return this._providerDisplayName;
|
||||
}
|
||||
|
||||
if (!this.providerId) {
|
||||
return SqlProviderEntry.getDefaultLabel();
|
||||
}
|
||||
// Note: consider adding API to connection management service to
|
||||
// support getting display name for provider so this is consistent
|
||||
switch (this.providerId) {
|
||||
case 'MSSQL':
|
||||
return 'MSSQL';
|
||||
default:
|
||||
return this.providerId;
|
||||
}
|
||||
}
|
||||
|
||||
public static getDefaultLabel(): string {
|
||||
return nls.localize('chooseSqlLang', 'Choose SQL Language');
|
||||
}
|
||||
}
|
||||
|
||||
// Shows SQL flavor status in the editor
|
||||
export class SqlFlavorStatusbarItem implements IStatusbarItem {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private _flavorElement: HTMLElement;
|
||||
private _sqlStatusEditors: { [editorUri: string]: SqlProviderEntry };
|
||||
private _toDispose: IDisposable[];
|
||||
|
||||
constructor(
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IEditorService private _editorService: EditorServiceImpl,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
) {
|
||||
this._sqlStatusEditors = {};
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): IDisposable {
|
||||
this._element = append(container, $('.query-statusbar-group'));
|
||||
this._flavorElement = append(this._element, $('a.editor-status-selection'));
|
||||
this._flavorElement.title = nls.localize('changeProvider', "Change SQL language provider");
|
||||
this._flavorElement.onclick = () => this._onSelectionClick();
|
||||
hide(this._flavorElement);
|
||||
|
||||
this._toDispose = [];
|
||||
this._toDispose.push(
|
||||
this._connectionManagementService.onLanguageFlavorChanged((changeParams: DidChangeLanguageFlavorParams) => this._onFlavorChanged(changeParams)),
|
||||
this._editorService.onDidVisibleEditorsChange(() => this._onEditorsChanged()),
|
||||
this._editorService.onDidCloseEditor(event => this._onEditorClosed(event))
|
||||
);
|
||||
return combinedDisposable(this._toDispose);
|
||||
}
|
||||
|
||||
private _onSelectionClick() {
|
||||
const action = this._instantiationService.createInstance(ChangeFlavorAction, ChangeFlavorAction.ID, ChangeFlavorAction.LABEL);
|
||||
|
||||
action.run().then(null, errors.onUnexpectedError);
|
||||
action.dispose();
|
||||
}
|
||||
|
||||
private _onEditorClosed(event: IEditorCloseEvent): void {
|
||||
let uri = WorkbenchUtils.getEditorUri(event.editor);
|
||||
if (uri && uri in this._sqlStatusEditors) {
|
||||
// If active editor is being closed, hide the query status.
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (uri === currentUri) {
|
||||
hide(this._flavorElement);
|
||||
}
|
||||
}
|
||||
// note: intentionally not removing language flavor. This is preserved across close/open events at present
|
||||
// delete this._sqlStatusEditors[uri];
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorsChanged(): void {
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let uri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
|
||||
// Show active editor's language flavor status
|
||||
if (uri) {
|
||||
this._showStatus(uri);
|
||||
} else {
|
||||
hide(this._flavorElement);
|
||||
}
|
||||
} else {
|
||||
hide(this._flavorElement);
|
||||
}
|
||||
}
|
||||
|
||||
private _onFlavorChanged(changeParams: DidChangeLanguageFlavorParams): void {
|
||||
if (changeParams) {
|
||||
this._updateStatus(changeParams.uri, new SqlProviderEntry(changeParams.flavor));
|
||||
}
|
||||
}
|
||||
|
||||
// Update query status for the editor
|
||||
private _updateStatus(uri: string, newStatus: SqlProviderEntry): void {
|
||||
if (uri) {
|
||||
this._sqlStatusEditors[uri] = newStatus;
|
||||
this._showStatus(uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide query status for active editor
|
||||
private _showStatus(uri: string): void {
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (uri === currentUri) {
|
||||
let flavor: SqlProviderEntry = this._sqlStatusEditors[uri];
|
||||
if (flavor) {
|
||||
this._flavorElement.textContent = flavor.label;
|
||||
} else {
|
||||
this._flavorElement.textContent = SqlProviderEntry.getDefaultLabel();
|
||||
}
|
||||
show(this._flavorElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeFlavorAction extends Action {
|
||||
|
||||
public static ID = 'sql.action.editor.changeProvider';
|
||||
public static LABEL = nls.localize('changeSqlProvider', "Change SQL Engine Provider");
|
||||
|
||||
constructor(
|
||||
actionId: string,
|
||||
actionLabel: string,
|
||||
@IEditorService private _editorService: IEditorService,
|
||||
@IQuickInputService private _quickInputService: IQuickInputService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(actionId, actionLabel);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (this._connectionManagementService.isConnected(currentUri)) {
|
||||
let currentProvider = this._connectionManagementService.getProviderIdFromUri(currentUri);
|
||||
return this._showMessage(Severity.Info, nls.localize('alreadyConnected',
|
||||
"A connection using engine {0} exists. To change please disconnect or change connection", currentProvider));
|
||||
}
|
||||
const editorWidget = getCodeEditor(activeEditor);
|
||||
if (!editorWidget) {
|
||||
return this._showMessage(Severity.Info, nls.localize('noEditor', "No text editor active at this time"));
|
||||
}
|
||||
|
||||
// TODO #1334 use connectionManagementService.GetProviderNames here. The challenge is that the credentials provider is returned
|
||||
// so we need a way to filter this using a capabilities check, with isn't yet implemented
|
||||
const ProviderOptions: ISqlProviderEntry[] = [
|
||||
new SqlProviderEntry('MSSQL')
|
||||
];
|
||||
|
||||
// TODO: select the current language flavor
|
||||
return this._quickInputService.pick(ProviderOptions, { placeHolder: nls.localize('pickSqlProvider', "Select SQL Language Provider") }).then(provider => {
|
||||
if (provider) {
|
||||
activeEditor = this._editorService.activeControl;
|
||||
const editorWidget = getCodeEditor(activeEditor);
|
||||
if (editorWidget) {
|
||||
if (currentUri) {
|
||||
this._connectionManagementService.doChangeLanguageFlavor(currentUri, 'sql', provider.providerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _showMessage(sev: Severity, message: string): Promise<any> {
|
||||
this._notificationService.notify({
|
||||
severity: sev,
|
||||
message: message
|
||||
});
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
272
src/sql/workbench/parts/query/browser/flexibleSash.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import {
|
||||
IHorizontalSashLayoutProvider, IVerticalSashLayoutProvider,
|
||||
ISashEvent, Orientation, Sash
|
||||
} from 'vs/base/browser/ui/sash/sash';
|
||||
// There is no need to import the sash CSS - 'vs/base/browser/ui/sash/sash' already includes it
|
||||
|
||||
/**
|
||||
* Interface describing a sash that could be horizontal or vertical. This interface allows classes
|
||||
* using the sash to have UI logic that is agnostic of the orientation of the sash.
|
||||
*/
|
||||
export interface IFlexibleSash {
|
||||
|
||||
// Get the value of the CSS property denoted by getMajorPosition()
|
||||
getSplitPoint(): number;
|
||||
|
||||
// Sets the Dimension containing the height and width of the editor this sash will separate
|
||||
setDimenesion(dimension: Dimension);
|
||||
|
||||
// Re-calculates the width and height of the sash
|
||||
layout(): void;
|
||||
|
||||
// Hides the sash
|
||||
hide(): void;
|
||||
|
||||
// Shows/unhides the sash
|
||||
show(): void;
|
||||
|
||||
// Sets the top or left property of this sash
|
||||
setEdge(edge: number);
|
||||
|
||||
// Fired when the position of this sash changes
|
||||
onPositionChange: Event<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple Vertical Sash that computes the position of the sash when it is moved between the given dimension.
|
||||
* Triggers onPositionChange event when the position is changed. Implements IFlexibleSash to enable classes to be
|
||||
* agnostic of the fact that this sash is vertical.
|
||||
*/
|
||||
|
||||
|
||||
export class VerticalFlexibleSash extends Disposable implements IVerticalSashLayoutProvider, IFlexibleSash {
|
||||
|
||||
private sash: Sash;
|
||||
private ratio: number;
|
||||
private startPosition: number;
|
||||
private position: number;
|
||||
private dimension: Dimension;
|
||||
private top: number;
|
||||
|
||||
private _onPositionChange: Emitter<number> = new Emitter<number>();
|
||||
public get onPositionChange(): Event<number> { return this._onPositionChange.event; }
|
||||
|
||||
constructor(container: HTMLElement, private minWidth: number) {
|
||||
super();
|
||||
this.ratio = 0.5;
|
||||
this.top = 0;
|
||||
this.sash = new Sash(container, this);
|
||||
|
||||
this._register(this.sash.onDidStart(() => this.onSashDragStart()));
|
||||
this._register(this.sash.onDidChange((e: ISashEvent) => this.onSashDrag(e)));
|
||||
this._register(this.sash.onDidEnd(() => this.onSashDragEnd()));
|
||||
this._register(this.sash.onDidReset(() => this.onSashReset()));
|
||||
}
|
||||
|
||||
public getSplitPoint(): number {
|
||||
return this.getVerticalSashLeft();
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this.sash.layout();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this.sash.show();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.sash.hide();
|
||||
}
|
||||
|
||||
public getVerticalSashTop(): number {
|
||||
return this.top;
|
||||
}
|
||||
|
||||
public getVerticalSashLeft(): number {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
public getVerticalSashHeight(): number {
|
||||
return this.dimension.height;
|
||||
}
|
||||
|
||||
public setDimenesion(dimension: Dimension) {
|
||||
this.dimension = dimension;
|
||||
this.compute(this.ratio);
|
||||
}
|
||||
|
||||
public setEdge(edge: number) {
|
||||
this.top = edge;
|
||||
}
|
||||
|
||||
private onSashDragStart(): void {
|
||||
this.startPosition = this.position;
|
||||
}
|
||||
|
||||
private onSashDrag(e: ISashEvent): void {
|
||||
this.compute((this.startPosition + (e.currentX - e.startX)) / this.dimension.width);
|
||||
}
|
||||
|
||||
private compute(ratio: number) {
|
||||
this.computeSashPosition(ratio);
|
||||
this.ratio = this.position / this.dimension.width;
|
||||
this._onPositionChange.fire(this.position);
|
||||
}
|
||||
|
||||
private onSashDragEnd(): void {
|
||||
this.sash.layout();
|
||||
}
|
||||
|
||||
private onSashReset(): void {
|
||||
this.ratio = 0.5;
|
||||
this._onPositionChange.fire(this.position);
|
||||
this.sash.layout();
|
||||
}
|
||||
|
||||
private computeSashPosition(sashRatio: number = this.ratio) {
|
||||
let contentWidth = this.dimension.width;
|
||||
let sashPosition = Math.floor((sashRatio || 0.5) * contentWidth);
|
||||
let midPoint = Math.floor(0.5 * contentWidth);
|
||||
|
||||
if (contentWidth > this.minWidth * 2) {
|
||||
if (sashPosition < this.minWidth) {
|
||||
sashPosition = this.minWidth;
|
||||
}
|
||||
if (sashPosition > contentWidth - this.minWidth) {
|
||||
sashPosition = contentWidth - this.minWidth;
|
||||
}
|
||||
} else {
|
||||
sashPosition = midPoint;
|
||||
}
|
||||
if (this.position !== sashPosition) {
|
||||
this.position = sashPosition;
|
||||
this.sash.layout();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple Horizontal Sash that computes the position of the sash when it is moved between the given dimension.
|
||||
* Triggers onPositionChange event when the position is changed. Implements IFlexibleSash to enable classes to be
|
||||
* agnostic of the fact that this sash is horizontal. Based off the VSash class.
|
||||
*/
|
||||
export class HorizontalFlexibleSash extends Disposable implements IHorizontalSashLayoutProvider, IFlexibleSash {
|
||||
|
||||
private static initialRatio: number = 0.4;
|
||||
private sash: Sash;
|
||||
private ratio: number;
|
||||
private startPosition: number;
|
||||
private position: number;
|
||||
private dimension: Dimension;
|
||||
private left: number;
|
||||
|
||||
private _onPositionChange: Emitter<number> = new Emitter<number>();
|
||||
public get onPositionChange(): Event<number> { return this._onPositionChange.event; }
|
||||
|
||||
constructor(container: HTMLElement, private minHeight: number) {
|
||||
super();
|
||||
this.ratio = HorizontalFlexibleSash.initialRatio;
|
||||
this.left = 0;
|
||||
this.sash = new Sash(container, this, { orientation: Orientation.HORIZONTAL });
|
||||
|
||||
this._register(this.sash.onDidStart(() => this.onSashDragStart()));
|
||||
this._register(this.sash.onDidChange((e: ISashEvent) => this.onSashDrag(e)));
|
||||
this._register(this.sash.onDidEnd(() => this.onSashDragEnd()));
|
||||
this._register(this.sash.onDidReset(() => this.onSashReset()));
|
||||
}
|
||||
|
||||
public getSplitPoint(): number {
|
||||
return this.getHorizontalSashTop();
|
||||
}
|
||||
|
||||
public getHorizontalSashLeft(): number {
|
||||
return this.left;
|
||||
}
|
||||
|
||||
public getHorizontalSashTop(): number {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this.sash.layout();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this.sash.show();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.sash.hide();
|
||||
}
|
||||
|
||||
public getHorizontalSashWidth?(): number {
|
||||
return this.dimension.width;
|
||||
}
|
||||
|
||||
public setDimenesion(dimension: Dimension) {
|
||||
this.dimension = dimension;
|
||||
this.compute(this.ratio);
|
||||
}
|
||||
|
||||
public setEdge(edge: number) {
|
||||
this.left = edge;
|
||||
}
|
||||
|
||||
private onSashDragStart(): void {
|
||||
this.startPosition = this.position;
|
||||
}
|
||||
|
||||
private onSashDrag(e: ISashEvent): void {
|
||||
this.compute((this.startPosition + (e.currentY - e.startY)) / this.dimension.height);
|
||||
}
|
||||
|
||||
private compute(ratio: number) {
|
||||
this.computeSashPosition(ratio);
|
||||
this.ratio = this.position / this.dimension.height;
|
||||
this._onPositionChange.fire(this.position);
|
||||
}
|
||||
|
||||
private onSashDragEnd(): void {
|
||||
this.sash.layout();
|
||||
}
|
||||
|
||||
private onSashReset(): void {
|
||||
this.ratio = HorizontalFlexibleSash.initialRatio;
|
||||
this._onPositionChange.fire(this.position);
|
||||
this.sash.layout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes where the sash should be located and re-renders the sash.
|
||||
*/
|
||||
private computeSashPosition(sashRatio: number = this.ratio) {
|
||||
let contentHeight = this.dimension.height;
|
||||
let sashPosition = Math.floor((sashRatio || 0.5) * contentHeight);
|
||||
let midPoint = Math.floor(0.5 * contentHeight);
|
||||
|
||||
if (contentHeight > this.minHeight * 4) {
|
||||
if (sashPosition < this.minHeight) {
|
||||
sashPosition = this.minHeight;
|
||||
}
|
||||
if (sashPosition > contentHeight - this.minHeight) {
|
||||
sashPosition = contentHeight - this.minHeight;
|
||||
}
|
||||
} else {
|
||||
sashPosition = midPoint;
|
||||
}
|
||||
if (this.position !== sashPosition) {
|
||||
this.position = sashPosition;
|
||||
this.sash.layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
455
src/sql/workbench/parts/query/browser/keyboardQueryActions.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { QueryEditor } from 'sql/workbench/parts/query/browser/queryEditor';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import * as Constants from 'sql/workbench/parts/query/common/constants';
|
||||
import * as ConnectionConstants from 'sql/platform/connection/common/constants';
|
||||
import { EditDataEditor } from 'sql/workbench/parts/editData/browser/editDataEditor';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
const singleQuote = '\'';
|
||||
|
||||
function isConnected(editor: QueryEditor, connectionManagementService: IConnectionManagementService): boolean {
|
||||
if (!editor || !editor.currentQueryInput) {
|
||||
return false;
|
||||
}
|
||||
return connectionManagementService.isConnected(editor.currentQueryInput.uri);
|
||||
}
|
||||
|
||||
function runActionOnActiveQueryEditor(editorService: IEditorService, action: (QueryEditor) => void): void {
|
||||
const candidates = [editorService.activeControl, ...editorService.visibleControls].filter(e => e instanceof QueryEditor);
|
||||
if (candidates.length > 0) {
|
||||
action(candidates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeSqlString(input: string, escapeChar: string) {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
let output = '';
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
let char = input.charAt(i);
|
||||
output += char;
|
||||
if (escapeChar === char) {
|
||||
output += char;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Locates the active editor and call focus() on the editor if it is a QueryEditor.
|
||||
*/
|
||||
export class FocusOnCurrentQueryKeyboardAction extends Action {
|
||||
|
||||
public static ID = 'focusOnCurrentQueryKeyboardAction';
|
||||
public static LABEL = nls.localize('focusOnCurrentQueryKeyboardAction', 'Focus on Current Query');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
queryEditor.focus();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the active editor and calls runQuery() on the editor if it is a QueryEditor.
|
||||
*/
|
||||
export class RunQueryKeyboardAction extends Action {
|
||||
|
||||
public static ID = 'runQueryKeyboardAction';
|
||||
public static LABEL = nls.localize('runQueryKeyboardAction', 'Run Query');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && (editor instanceof QueryEditor || editor instanceof EditDataEditor)) {
|
||||
let queryEditor: QueryEditor | EditDataEditor = editor;
|
||||
queryEditor.runQuery();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the active editor and calls runCurrentQuery() on the editor if it is a QueryEditor.
|
||||
*/
|
||||
export class RunCurrentQueryKeyboardAction extends Action {
|
||||
public static ID = 'runCurrentQueryKeyboardAction';
|
||||
public static LABEL = nls.localize('runCurrentQueryKeyboardAction', 'Run Current Query');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
queryEditor.runCurrentQuery();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class RunCurrentQueryWithActualPlanKeyboardAction extends Action {
|
||||
public static ID = 'runCurrentQueryWithActualPlanKeyboardAction';
|
||||
public static LABEL = nls.localize('runCurrentQueryWithActualPlanKeyboardAction', 'Run Current Query with Actual Plan');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
queryEditor.runCurrentQueryWithActualPlan();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the active editor and calls cancelQuery() on the editor if it is a QueryEditor.
|
||||
*/
|
||||
export class CancelQueryKeyboardAction extends Action {
|
||||
|
||||
public static ID = 'cancelQueryKeyboardAction';
|
||||
public static LABEL = nls.localize('cancelQueryKeyboardAction', 'Cancel Query');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && (editor instanceof QueryEditor || editor instanceof EditDataEditor)) {
|
||||
let queryEditor: QueryEditor | EditDataEditor = editor;
|
||||
queryEditor.cancelQuery();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the IntelliSense cache
|
||||
*/
|
||||
export class RefreshIntellisenseKeyboardAction extends Action {
|
||||
public static ID = 'refreshIntellisenseKeyboardAction';
|
||||
public static LABEL = nls.localize('refreshIntellisenseKeyboardAction', 'Refresh IntelliSense Cache');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
queryEditor.rebuildIntelliSenseCache();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hide the query results
|
||||
*/
|
||||
export class ToggleQueryResultsKeyboardAction extends Action {
|
||||
public static ID = 'toggleQueryResultsKeyboardAction';
|
||||
public static LABEL = nls.localize('toggleQueryResultsKeyboardAction', 'Toggle Query Results');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
queryEditor.toggleResultsEditorVisibility();
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that runs a query in the active SQL text document.
|
||||
*/
|
||||
export class RunQueryShortcutAction extends Action {
|
||||
public static ID = 'runQueryShortcutAction';
|
||||
|
||||
constructor(
|
||||
@IEditorService private _editorService: IEditorService,
|
||||
@IQueryModelService protected _queryModelService: IQueryModelService,
|
||||
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IConfigurationService private _workspaceConfigurationService: IConfigurationService
|
||||
) {
|
||||
super(RunQueryShortcutAction.ID);
|
||||
}
|
||||
|
||||
public run(index: number): Promise<void> {
|
||||
let promise: Thenable<void> = Promise.resolve(null);
|
||||
runActionOnActiveQueryEditor(this._editorService, (editor) => {
|
||||
promise = this.runQueryShortcut(editor, index);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
promise.then(success => resolve(null), err => resolve(null));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one of the optionally registered query shortcuts. This will lookup the shortcut's stored procedure
|
||||
* reference from the settings, and if found will execute it plus any
|
||||
*
|
||||
* @param shortcutIndex which shortcut should be run?
|
||||
*/
|
||||
public runQueryShortcut(editor: QueryEditor, shortcutIndex: number): Thenable<void> {
|
||||
if (!editor) {
|
||||
throw new Error(nls.localize('queryShortcutNoEditor', 'Editor parameter is required for a shortcut to be executed'));
|
||||
}
|
||||
|
||||
if (isConnected(editor, this._connectionManagementService)) {
|
||||
let shortcutText = this.getShortcutText(shortcutIndex);
|
||||
if (!shortcutText.trim()) {
|
||||
// no point going further
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// if the selection isn't empty then execute the selection
|
||||
// otherwise, either run the statement or the script depending on parameter
|
||||
let parameterText: string = editor.getSelectionText();
|
||||
return this.escapeStringParamIfNeeded(editor, shortcutText, parameterText).then((escapedParam) => {
|
||||
let queryString = `${shortcutText} ${escapedParam}`;
|
||||
editor.currentQueryInput.runQueryString(queryString);
|
||||
}).then(success => null, err => {
|
||||
// swallow errors for now
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
private getShortcutText(shortcutIndex: number) {
|
||||
let shortcutSetting = Constants.shortcutStart + shortcutIndex;
|
||||
let querySettings = WorkbenchUtils.getSqlConfigSection(this._workspaceConfigurationService, Constants.querySection);
|
||||
let shortcutText = querySettings[shortcutSetting];
|
||||
return shortcutText;
|
||||
}
|
||||
|
||||
private escapeStringParamIfNeeded(editor: QueryEditor, shortcutText: string, parameterText: string): Thenable<string> {
|
||||
if (parameterText && parameterText.length > 0) {
|
||||
if (this.canQueryProcMetadata(editor)) {
|
||||
let dbName = this.getDatabaseName(editor);
|
||||
let query = `exec dbo.sp_sproc_columns @procedure_name = N'${escapeSqlString(shortcutText, singleQuote)}', @procedure_owner = null, @procedure_qualifier = N'${escapeSqlString(dbName, singleQuote)}'`;
|
||||
return this._queryManagementService.runQueryAndReturn(editor.uri, query)
|
||||
.then(result => {
|
||||
switch (this.isProcWithSingleArgument(result)) {
|
||||
case 1:
|
||||
// sproc was found and it meets criteria of having 1 string param
|
||||
// if selection is quoted, leave as-is. Else quote
|
||||
let trimmedText = parameterText.trim();
|
||||
if (trimmedText.length > 0) {
|
||||
if (trimmedText.charAt(0) !== singleQuote || trimmedText.charAt(trimmedText.length - 1) !== singleQuote) {
|
||||
// Note: SSMS uses the original text, but this causes issues if you have spaces. We intentionally use
|
||||
// trimmed text since it's likely to be more accurate in this case. For non-quoted cases it shouldn't matter
|
||||
return `'${trimmedText}'`;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case -1:
|
||||
// sproc was found but didn't meet criteria, so append as-is
|
||||
case 0:
|
||||
// sproc wasn't found, just append as-is and hope it works
|
||||
break;
|
||||
}
|
||||
return parameterText;
|
||||
}, err => {
|
||||
return parameterText;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(parameterText);
|
||||
}
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
private isProcWithSingleArgument(result: azdata.SimpleExecuteResult): number {
|
||||
let columnTypeOrdinal = this.getColumnIndex(result.columnInfo, 'COLUMN_TYPE');
|
||||
let dataTypeOrdinal = this.getColumnIndex(result.columnInfo, 'DATA_TYPE');
|
||||
if (columnTypeOrdinal && dataTypeOrdinal) {
|
||||
let count = 0;
|
||||
for (let row of result.rows) {
|
||||
let columnType = parseInt(row[columnTypeOrdinal].displayValue);
|
||||
if (columnType !== 5) {
|
||||
if (count > 0) // more than one argument.
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
let dataType = parseInt(row[dataTypeOrdinal].displayValue);
|
||||
|
||||
if (dataType === -9 || // nvarchar
|
||||
dataType === 12 || // varchar
|
||||
dataType === -8 || // nchar
|
||||
dataType === 1 || // char
|
||||
dataType === -1 || // text
|
||||
dataType === -10 // ntext
|
||||
) {
|
||||
count++;
|
||||
} else {
|
||||
// not a string
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
return -1; // Couldn't process so return default value
|
||||
}
|
||||
|
||||
private getColumnIndex(columnInfo: azdata.IDbColumn[], columnName: string): number {
|
||||
return columnInfo ? columnInfo.findIndex(c => c.columnName === columnName) : undefined;
|
||||
}
|
||||
|
||||
private canQueryProcMetadata(editor: QueryEditor): boolean {
|
||||
let info = this._connectionManagementService.getConnectionInfo(editor.uri);
|
||||
return (info && info.providerId === ConnectionConstants.mssqlProviderName);
|
||||
}
|
||||
|
||||
private getDatabaseName(editor: QueryEditor): string {
|
||||
let info = this._connectionManagementService.getConnectionInfo(editor.uri);
|
||||
return info.connectionProfile.databaseName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that parses the query string in the current SQL text document.
|
||||
*/
|
||||
export class ParseSyntaxAction extends Action {
|
||||
|
||||
public static ID = 'parseQueryAction';
|
||||
public static LABEL = nls.localize('parseSyntaxLabel', 'Parse Query');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
||||
@IEditorService private _editorService: IEditorService,
|
||||
@INotificationService private _notificationService: INotificationService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
let editor = this._editorService.activeControl;
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
if (!queryEditor.isSelectionEmpty()) {
|
||||
if (this.isConnected(queryEditor)) {
|
||||
let text = queryEditor.getSelectionText();
|
||||
if (text === '') {
|
||||
text = queryEditor.getAllText();
|
||||
}
|
||||
this._queryManagementService.parseSyntax(queryEditor.connectedUri, text).then(result => {
|
||||
if (result && result.parseable) {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Info,
|
||||
message: nls.localize('queryActions.parseSyntaxSuccess', 'Commands completed successfully')
|
||||
});
|
||||
} else if (result && result.errors.length > 0) {
|
||||
let errorMessage = nls.localize('queryActions.parseSyntaxFailure', 'Command failed: ');
|
||||
this._notificationService.error(`${errorMessage}${result.errors[0]}`);
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('queryActions.notConnected', 'Please connect to a server')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI of the given editor if it is not undefined and is connected.
|
||||
* Public for testing only.
|
||||
*/
|
||||
private isConnected(editor: QueryEditor): boolean {
|
||||
if (!editor || !editor.currentQueryInput) {
|
||||
return false;
|
||||
}
|
||||
return this._connectionManagementService.isConnected(editor.currentQueryInput.uri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .binarydiff-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.monaco-workbench .binarydiff-right {
|
||||
border-left: 3px solid #DDD;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .binarydiff-right {
|
||||
border-left: 3px solid rgb(20, 20, 20);
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .binarydiff-right {
|
||||
border-left: 3px solid #6FC3DF;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><circle fill="#C5C5C5" cx="8" cy="8" r="4"/></svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" enable-background="new 0 0 16 16" height="16" width="16"><circle fill="#424242" cx="8" cy="8" r="4"/></svg>
|
||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
1
src/sql/workbench/parts/query/browser/media/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vs .monaco-workbench > .editor > .content.drag {
|
||||
background-color: #ECECEC;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .editor > .content.drag {
|
||||
background-color: #2D2D2D;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.dragging > .monaco-sash {
|
||||
display: none; /* hide sashes while dragging editors around */
|
||||
}
|
||||
|
||||
#monaco-workbench-editor-move-overlay,
|
||||
#monaco-workbench-editor-drop-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#monaco-workbench-editor-drop-overlay {
|
||||
opacity: 0; /* initially not visible until moving around */
|
||||
}
|
||||
|
||||
.vs #monaco-workbench-editor-drop-overlay,
|
||||
.vs .monaco-workbench > .editor.empty > .content.dropfeedback {
|
||||
background-color: rgba(51,153,255, 0.18);
|
||||
}
|
||||
|
||||
.vs-dark #monaco-workbench-editor-drop-overlay,
|
||||
.vs-dark .monaco-workbench > .editor.empty > .content.dropfeedback {
|
||||
background-color: rgba(83, 89, 93, 0.5);
|
||||
}
|
||||
|
||||
.hc-black #monaco-workbench-editor-drop-overlay,
|
||||
.hc-black .monaco-workbench > .editor.empty > .content.dropfeedback {
|
||||
background: none !important;
|
||||
outline: 2px dashed #f38518;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content > .one-editor-silo {
|
||||
position: absolute;
|
||||
box-sizing: border-box; /* use border box to be able to draw a border as separator between editors */
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content > .one-editor-silo.editor-one {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content > .one-editor-silo.dragging {
|
||||
z-index: 70;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.dragging {
|
||||
border-left: 1px solid #E7E7E7;
|
||||
border-right: 1px solid #E7E7E7;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.dragging {
|
||||
border-top: 1px solid #E7E7E7;
|
||||
border-bottom: 1px solid #E7E7E7;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-two,
|
||||
.vs .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three {
|
||||
border-left: 1px solid #E7E7E7;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-two,
|
||||
.vs .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three {
|
||||
border-top: 1px solid #E7E7E7;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.dragging {
|
||||
border-left: 1px solid #444;
|
||||
border-right: 1px solid #444;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.dragging {
|
||||
border-top: 1px solid #444;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-two,
|
||||
.vs-dark .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three {
|
||||
border-left: 1px solid #444;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-two,
|
||||
.vs-dark .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three {
|
||||
border-top: 1px solid #444;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.dragging {
|
||||
border-left: 1px solid #6FC3DF;
|
||||
border-right: 1px solid #6FC3DF;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.dragging {
|
||||
border-top: 1px solid #6FC3DF;
|
||||
border-bottom: 1px solid #6FC3DF;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-two,
|
||||
.hc-black .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three {
|
||||
border-left: 1px solid #6FC3DF;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-two,
|
||||
.hc-black .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three {
|
||||
border-top: 1px solid #6FC3DF;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.draggedunder {
|
||||
transition: left 200ms ease-out;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.vertical-layout > .editor-three.draggedunder {
|
||||
transition-property: right;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.draggedunder {
|
||||
transition: top 200ms ease-out;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content.horizontal-layout > .editor-three.draggedunder {
|
||||
transition-property: bottom;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content > .one-editor-silo > .container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-workbench > .editor > .content > .one-editor-silo > .container > .editor-container {
|
||||
height: calc(100% - 35px); /* Editor is below editor title */
|
||||
}
|
||||
41
src/sql/workbench/parts/query/browser/media/editorpart.css
Normal file
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vs .monaco-workbench .monaco-editor-background {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .monaco-editor-background {
|
||||
background-color: #1E1E1E;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .monaco-editor-background {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor {
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor.empty {
|
||||
background-image: url('letterpress.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .part.editor.empty {
|
||||
background-image: url('letterpress-dark.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .part.editor.empty {
|
||||
background-image: url('letterpress-hc.svg');
|
||||
}
|
||||
|
||||
@media
|
||||
(-webkit-min-device-pixel-ratio: 2),
|
||||
(min-resolution: 192dppx) {
|
||||
.monaco-workbench .part.editor {
|
||||
background-size: 260px 260px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .quick-open-widget .quick-open-tree .quick-open-entry.editor-preview {
|
||||
font-style: italic;
|
||||
}
|
||||
26
src/sql/workbench/parts/query/browser/media/editorstatus.css
Normal file
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .editor-statusbar-item > a:not(:first-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-mode,
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-encoding,
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-eol,
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-selection,
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-indentation,
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-metadata {
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-metadata {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-workbench .editor-statusbar-item > .editor-status-tabfocusmode {
|
||||
padding: 0 5px 0 5px;
|
||||
background-color: brown !important;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .query-statusbar-group > .editor-status-selection {
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" width="260" height="260" viewBox="0 0 260 260"><style>.st0{opacity:.25}.st1{opacity:3e-2}.st2{fill:#fff}</style><path d="M194 2L92.4 103.6 27.6 53.2 2 66v128l25.6 12.8 64.8-50.4L194 258l64-25.6V27.6L194 2zM27 169V91l39 39-39 39zm99.4-39L194 77v106l-67.6-53z" class="st0"/><path class="st1 st2" d="M194 2l64 25.6v204.8L194 258 92.4 156.4l-64.8 50.4L2 194V66l25.6-12.8 64.8 50.4L194 2m0 181V77l-67.6 53 67.6 53M27 169l39-39-39-39v78M193.8.8l-.5.5-101 101-64.1-49.9-.5-.4-.6.3L1.6 65.1l-.6.3v129.2l.6.3 25.6 12.8.6.3.5-.4 64.1-49.9 101 101 .5.5.6-.2 64-25.6.6-.3V26.9l-.6-.3-64-25.6-.7-.2zM128 130l65-50.9V181l-65-51zM28 166.6V93.4L64.6 130 28 166.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 709 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" width="260" height="260" viewBox="0 0 260 260"><style>.st0{fill:#fff;fill-opacity:.13;enable-background:new}</style><path class="st0" d="M194 2L92.4 103.6 27.6 53.2 2 66v128l25.6 12.8 64.8-50.4L194 258l64-25.6V27.6L194 2zM27 169V91l39 39-39 39zm99.4-39L194 77v106l-67.6-53z"/></svg>
|
||||
|
After Width: | Height: | Size: 335 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" width="260" height="260" viewBox="0 0 260 260"><style>.st0{opacity:.1}.st1{opacity:5e-2}.st2{fill:#231f20}</style><path d="M194 2L92.4 103.6 27.6 53.2 2 66v128l25.6 12.8 64.8-50.4L194 258l64-25.6V27.6L194 2zM27 169V91l39 39-39 39zm99.4-39L194 77v106l-67.6-53z" class="st0"/><path class="st1 st2" d="M194 2l64 25.6v204.8L194 258 92.4 156.4l-64.8 50.4L2 194V66l25.6-12.8 64.8 50.4L194 2m0 181V77l-67.6 53 67.6 53M27 169l39-39-39-39v78M193.8.8l-.5.5-101 101-64.1-49.9-.5-.4-.6.3L1.6 65.1l-.6.3v129.2l.6.3 25.6 12.8.6.3.5-.4 64.1-49.9 101 101 .5.5.6-.2 64-25.6.6-.3V26.9l-.6-.3-64-25.6-.7-.2zM128 130l65-50.9V181l-65-51zM28 166.6V93.4L64.6 130 28 166.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 711 B |
46
src/sql/workbench/parts/query/browser/media/messagePanel.css
Normal file
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Disable repl hover highlight in tree. */
|
||||
.monaco-workbench .message-tree .monaco-tree .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
/* Disable repl hover highlight in tree. */
|
||||
.monaco-workbench .message-tree .monaco-tree .monaco-tree-row > .content {
|
||||
line-height: 18px;
|
||||
user-select: text;
|
||||
word-wrap: break-word;
|
||||
/* white-space: pre-wrap; */
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.monaco-workbench .message-tree .monaco-tree .monaco-tree-rows>.monaco-tree-row {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.message-tree .time-stamp {
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message-tree .message,
|
||||
.message-tree .batch-start,
|
||||
.message-tree .error-message {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message-tree .batch-start {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-tree .error-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.message-tree .batch-start:hover {
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16"><path fill="#C5C5C5" d="M1 4h7l-3-3h3l4 4-4 4h-3l3-3h-7v-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16"><path fill="#656565" d="M1 4h7l-3-3h3l4 4-4 4h-3l3-3h-7v-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 189 B |
47
src/sql/workbench/parts/query/browser/media/notabstitle.css
Normal file
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Title Label */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label {
|
||||
line-height: 35px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .monaco-icon-label::before {
|
||||
height: 35px; /* tweak the icon size of the editor labels when icons are enabled */
|
||||
}
|
||||
|
||||
/* Title Actions */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions {
|
||||
display: flex;
|
||||
flex: initial;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .title-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action {
|
||||
background: url('close-dirty.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action {
|
||||
background: url('close-dirty-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action:hover {
|
||||
background: url('close.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action:hover,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action:hover {
|
||||
background: url('close-inverse.svg') center center no-repeat;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>ParseQuery_16x</title><rect width="16" height="16" fill="#f6f6f6" opacity="0"/><polygon points="4.382 15 0.382 7 5.618 7 6.5 8.764 10.382 1 15.618 1 8.618 15 4.382 15" fill="#f6f6f6"/><polygon points="11 2 6.5 11 5 8 2 8 5 14 8 14 14 2 11 2" fill="#424242"/></svg>
|
||||
|
After Width: | Height: | Size: 331 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16"><polygon fill="#C5C5C5" points="13,4 6,4 9,1 6,1 2,5 6,9 9,9 6,6 13,6"/></svg>
|
||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16"><polygon fill="#656565" points="13,4 6,4 9,1 6,1 2,5 6,9 9,9 6,6 13,6"/></svg>
|
||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.databaseListDropdown {
|
||||
min-width: 150px;
|
||||
}
|
||||
29
src/sql/workbench/parts/query/browser/media/queryEditor.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vs-dark .side-by-side-editor > .master-editor-container {
|
||||
box-shadow: -6px 0 5px -5px black;
|
||||
}
|
||||
|
||||
.side-by-side-editor > .master-editor-container {
|
||||
box-shadow: -6px 0 5px -5px #DDD;
|
||||
}
|
||||
|
||||
.vs-dark .side-by-side-editor > .master-editor-container-horizontal {
|
||||
box-shadow: 0 -6px 5px -5px black;
|
||||
}
|
||||
|
||||
.side-by-side-editor > .master-editor-container-horizontal {
|
||||
box-shadow: 0 -6px 5px -5px #DDD;
|
||||
}
|
||||
|
||||
.editDataEditor {
|
||||
height: inherit
|
||||
}
|
||||
|
||||
#chartViewerDiv .chartViewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2{fill:#f6f6f6;}.cls-1{opacity:0;}.cls-3{fill:#388a34;}.cls-4{fill:#f0eff1;}</style></defs><title>StartWithoutDebug@2x</title><g id="Layer_2" data-name="Layer 2"><g id="outline"><rect class="cls-1" width="16" height="16"/><path class="cls-2" d="M3,0,13.67,8,3,16Z"/></g><g id="color_action"><path class="cls-3" d="M6,6,8.67,8,6,10V6M4,2V14l8-6L4,2Z"/></g><g id="icon_fg"><path class="cls-4" d="M8.67,8,6,10V6Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vs-dark .side-by-side-editor > .master-editor-container {
|
||||
box-shadow: -6px 0 5px -5px black;
|
||||
}
|
||||
|
||||
.side-by-side-editor > .master-editor-container {
|
||||
box-shadow: -6px 0 5px -5px #DDD;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 15H0V1h16v14z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M1 2v12h14V2H1zm13 11H2v-3h12v3zm0-5H2V5h12v3z" id="iconBg"/><g id="iconFg" style="display: none;"><path class="icon-vs-fg" d="M14 8H2V5h12v3zm0 2H2v3h12v-3z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 15H0V1h16v14z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M1 2v12h14V2H1zm13 11H2v-3h12v3zm0-5H2V5h12v3z" id="iconBg"/><g id="iconFg" style="display: none;"><path class="icon-vs-fg" d="M14 8H2V5h12v3zm0 2H2v3h12v-3z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" enable-background="new 0 -1 16 16"><path fill="#C5C5C5" d="M1 1v12h14v-12h-14zm1 3h4.999v8h-4.999v-8zm12 8h-5.001v-8h5.001v8z"/></svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -1 16 16" enable-background="new 0 -1 16 16"><path fill="#656565" d="M1 1v12h14v-12h-14zm1 3h4.999v8h-4.999v-8zm12 8h-5.001v-8h5.001v8z"/></svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#C5C5C5;}
|
||||
</style>
|
||||
<g id="outline">
|
||||
</g>
|
||||
<g id="icon_x5F_bg">
|
||||
<path class="st0" d="M7.6,5H3c0,0-1,0-1,1c0,0.8,0,5.4,0,8c0,1,1,1,1,1s1.5,0,3,0s3,0,3,0s1,0,1-1c0-2.6,0-6.9,0-6.9L7.6,5z M9,14
|
||||
H3V6h4v2h2V14z"/>
|
||||
<path class="st0" d="M9.6,3H5c0,0-1,0-1,1h5v0.9L10.2,6H11v7c1,0,1-1,1-1V5.1L9.6,3z"/>
|
||||
<path class="st0" d="M11.6,1H7c0,0-1,0-1,1h5v0.9L12.2,4H13v7c1,0,1-1,1-1V3.1L11.6,1z"/>
|
||||
</g>
|
||||
<g id="color_x5F_action">
|
||||
</g>
|
||||
<g id="icon_x5F_fg">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
20
src/sql/workbench/parts/query/browser/media/stackview.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#656565;}
|
||||
</style>
|
||||
<g id="outline">
|
||||
</g>
|
||||
<g id="icon_x5F_bg">
|
||||
<path class="st0" d="M7.6,5H3c0,0-1,0-1,1c0,0.8,0,5.4,0,8c0,1,1,1,1,1s1.5,0,3,0s3,0,3,0s1,0,1-1c0-2.6,0-6.9,0-6.9L7.6,5z M9,14
|
||||
H3V6h4v2h2V14z"/>
|
||||
<path class="st0" d="M9.6,3H5c0,0-1,0-1,1h5v0.9L10.2,6H11v7c1,0,1-1,1-1V5.1L9.6,3z"/>
|
||||
<path class="st0" d="M11.6,1H7c0,0-1,0-1,1h5v0.9L12.2,4H13v7c1,0,1-1,1-1V3.1L11.6,1z"/>
|
||||
</g>
|
||||
<g id="color_x5F_action">
|
||||
</g>
|
||||
<g id="icon_x5F_fg">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
220
src/sql/workbench/parts/query/browser/media/tabstitle.css
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Title Container */
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs {
|
||||
background: #F3F3F3;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs {
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs > .monaco-scrollable-element {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs > .monaco-scrollable-element .scrollbar {
|
||||
z-index: 3; /* on top of tabs */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Tabs Container */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container {
|
||||
display: flex;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.scroll {
|
||||
overflow: scroll !important;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Tab */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab {
|
||||
display: flex;
|
||||
width: 120px;
|
||||
min-width: fit-content;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
height: 35px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid transparent;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) {
|
||||
background-color: #ECECEC;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) {
|
||||
background-color: #2D2D2D;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab {
|
||||
border-left-color: #F3F3F3;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child {
|
||||
border-right-color: #F3F3F3;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab {
|
||||
border-left-color: #252526;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child {
|
||||
border-right-color: #252526;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child,
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child {
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab {
|
||||
border-left-color: #6FC3DF;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active {
|
||||
outline: 2px solid #f38518;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback,
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback {
|
||||
background-color: #DDECFF;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback,
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback {
|
||||
background-color: #383B3D;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback {
|
||||
background: none !important;
|
||||
outline: 2px dashed #f38518;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Tab Label */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .monaco-icon-label::before {
|
||||
height: 16px; /* tweak the icon size of the editor labels when icons are enabled */
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label {
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active .tab-label,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback .tab-label {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Tab Close */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab > .tab-close {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button > .tab-close {
|
||||
display: none; /* hide the close action bar when we are configured to hide it */
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab > .tab-close .action-label:focus, /* always show it on focus */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab:hover > .tab-close .action-label, /* always show it on hover */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab.dirty > .tab-close .action-label { /* always show it for dirty tabs */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* show dimmed for inactive group */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty > .tab-close .action-label, /* show dimmed for inactive group */
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab > .tab-close .action-label {
|
||||
opacity: 0;
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action {
|
||||
background: url('close-dirty.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action {
|
||||
background: url('close-dirty-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover {
|
||||
background: url('close.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover {
|
||||
background: url('close-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
/* No Tab Close Button */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button {
|
||||
padding-right: 28px; /* make room for dirty indication when we are running without close button */
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty {
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: center;
|
||||
background-position-x: calc(100% - 6px); /* to the right of the tab label */
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty {
|
||||
background-image: url('close-dirty.svg');
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty {
|
||||
background-image: url('close-dirty-inverse.svg');
|
||||
}
|
||||
|
||||
/* Editor Actions */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions {
|
||||
cursor: default;
|
||||
flex: initial;
|
||||
padding-left: 4px;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vs .monaco-workbench .textdiff-editor-action.next {
|
||||
background: url('next-diff.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench .textdiff-editor-action.previous {
|
||||
background: url('previous-diff.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .textdiff-editor-action.next,
|
||||
.hc-black .monaco-workbench .textdiff-editor-action.next {
|
||||
background: url('next-diff-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .textdiff-editor-action.previous,
|
||||
.hc-black .monaco-workbench .textdiff-editor-action.previous {
|
||||
background: url('previous-diff-inverse.svg') center center no-repeat;
|
||||
}
|
||||
127
src/sql/workbench/parts/query/browser/media/titlecontrol.css
Normal file
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Editor Label */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label {
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a {
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .monaco-icon-label::before,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .monaco-icon-label::before,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label span,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label span {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a,
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a {
|
||||
color: rgba(51, 51, 51, 0.5);
|
||||
}
|
||||
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a,
|
||||
.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a,
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a,
|
||||
.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Title Actions */
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label {
|
||||
display: block;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
min-width: 28px;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label,
|
||||
.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label .label,
|
||||
.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label .label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Drag Cursor */
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title.tabs .scrollbar .slider,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title .monaco-icon-label::before,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title .title-label a,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title .title-label span {
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
#monaco-workbench-editor-move-overlay,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title.tabs .scrollbar .slider,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title .monaco-icon-label::before,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title .title-label a,
|
||||
.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title .title-label span {
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
.monaco-workbench .close-editor-action {
|
||||
background: url('close.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .close-editor-action,
|
||||
.hc-black .monaco-workbench .close-editor-action {
|
||||
background: url('close-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action {
|
||||
background: url('split-editor-vertical.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action,
|
||||
.hc-black .monaco-workbench > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action {
|
||||
background: url('split-editor-vertical-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action {
|
||||
background: url('split-editor-horizontal.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action,
|
||||
.hc-black .monaco-workbench > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action {
|
||||
background: url('split-editor-horizontal-inverse.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench .show-group-editors-action {
|
||||
background: url('stackview.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .show-group-editors-action,
|
||||
.hc-black .monaco-workbench .show-group-editors-action {
|
||||
background: url('stackview-inverse.svg') center center no-repeat;
|
||||
}
|
||||
428
src/sql/workbench/parts/query/browser/messagePanel.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/messagePanel';
|
||||
import { IMessagesActionContext, CopyMessagesAction, CopyAllMessagesAction } from './actions';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { IExpandableTree } from 'sql/workbench/parts/objectExplorer/browser/treeUpdateUtils';
|
||||
|
||||
import { IResultMessage, ISelectionData } from 'azdata';
|
||||
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { OpenMode, ClickBehavior, ICancelableEvent, IControllerOptions } from 'vs/base/parts/tree/browser/treeDefaults';
|
||||
import { WorkbenchTreeController } from 'vs/platform/list/browser/listService';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { isArray, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
|
||||
export interface IResultMessageIntern extends IResultMessage {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface IMessagePanelMessage {
|
||||
message: string;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export interface IMessagePanelBatchMessage extends IMessagePanelMessage {
|
||||
selection: ISelectionData;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface IMessageTemplate {
|
||||
message: HTMLElement;
|
||||
}
|
||||
|
||||
interface IBatchTemplate extends IMessageTemplate {
|
||||
timeStamp: HTMLElement;
|
||||
}
|
||||
|
||||
const TemplateIds = {
|
||||
MESSAGE: 'message',
|
||||
BATCH: 'batch',
|
||||
MODEL: 'model',
|
||||
ERROR: 'error'
|
||||
};
|
||||
|
||||
export class MessagePanelState {
|
||||
public scrollPosition: number;
|
||||
public collapsed = false;
|
||||
|
||||
constructor(@IConfigurationService configurationService: IConfigurationService) {
|
||||
let messagesOpenedSettings = configurationService.getValue<boolean>('sql.messagesDefaultOpen');
|
||||
if (!isUndefinedOrNull(messagesOpenedSettings)) {
|
||||
this.collapsed = !messagesOpenedSettings;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class MessagePanel extends ViewletPanel {
|
||||
private messageLineCountMap = new Map<IResultMessage, number>();
|
||||
private ds = new MessageDataSource();
|
||||
private renderer = new MessageRenderer(this.messageLineCountMap);
|
||||
private model = new Model();
|
||||
private controller: MessageController;
|
||||
private container = $('.message-tree');
|
||||
|
||||
private queryRunnerDisposables: IDisposable[] = [];
|
||||
private _state: MessagePanelState;
|
||||
|
||||
private tree: ITree;
|
||||
|
||||
constructor(
|
||||
options: IViewletPanelOptions,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IClipboardService private clipboardService: IClipboardService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService);
|
||||
this.controller = instantiationService.createInstance(MessageController, { openMode: OpenMode.SINGLE_CLICK, clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change, to preserve focus behaviour in input field */ });
|
||||
this.controller.toFocusOnClick = this.model;
|
||||
this.tree = new Tree(this.container, {
|
||||
dataSource: this.ds,
|
||||
renderer: this.renderer,
|
||||
controller: this.controller
|
||||
}, { keyboardSupport: false, horizontalScrollMode: ScrollbarVisibility.Auto });
|
||||
this.disposables.push(this.tree);
|
||||
this.tree.onDidScroll(e => {
|
||||
// convert to old VS Code tree interface with expandable methods
|
||||
let expandableTree: IExpandableTree = <IExpandableTree>this.tree;
|
||||
|
||||
if (this.state) {
|
||||
this.state.scrollPosition = expandableTree.getScrollPosition();
|
||||
}
|
||||
});
|
||||
this.onDidChange(e => {
|
||||
if (this.state) {
|
||||
this.state.collapsed = !this.isExpanded();
|
||||
}
|
||||
});
|
||||
this.controller.onKeyDown = (tree, event) => {
|
||||
if (event.ctrlKey) {
|
||||
let context: IMessagesActionContext = {
|
||||
selection: document.getSelection(),
|
||||
tree: this.tree,
|
||||
};
|
||||
// Ctrl + C for copy
|
||||
if (event.code === 'KeyC') {
|
||||
let copyMessageAction = instantiationService.createInstance(CopyMessagesAction, this.clipboardService);
|
||||
copyMessageAction.run(context);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
};
|
||||
this.controller.onContextMenu = (tree, element, event) => {
|
||||
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
|
||||
return false; // allow context menu on input fields
|
||||
}
|
||||
|
||||
// Prevent native context menu from showing up
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const selection = document.getSelection();
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => {
|
||||
return { x: event.posx, y: event.posy };
|
||||
},
|
||||
getActions: () => {
|
||||
return [
|
||||
instantiationService.createInstance(CopyMessagesAction, this.clipboardService),
|
||||
instantiationService.createInstance(CopyAllMessagesAction, this.tree, this.clipboardService)
|
||||
];
|
||||
},
|
||||
getActionsContext: () => {
|
||||
return <IMessagesActionContext>{
|
||||
selection,
|
||||
tree
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this.container.style.width = '100%';
|
||||
this.container.style.height = '100%';
|
||||
this.disposables.push(attachListStyler(this.tree, this.themeService));
|
||||
container.appendChild(this.container);
|
||||
this.tree.setInput(this.model);
|
||||
}
|
||||
|
||||
protected layoutBody(size: number): void {
|
||||
// convert to old VS Code tree interface with expandable methods
|
||||
let expandableTree: IExpandableTree = <IExpandableTree>this.tree;
|
||||
|
||||
const previousScrollPosition = expandableTree.getScrollPosition();
|
||||
this.tree.layout(size);
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
expandableTree.setScrollPosition(this.state.scrollPosition);
|
||||
} else {
|
||||
if (previousScrollPosition === 1) {
|
||||
expandableTree.setScrollPosition(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
dispose(this.queryRunnerDisposables);
|
||||
this.queryRunnerDisposables = [];
|
||||
this.reset();
|
||||
this.queryRunnerDisposables.push(runner.onQueryStart(() => this.reset()));
|
||||
this.queryRunnerDisposables.push(runner.onMessage(e => this.onMessage(e)));
|
||||
this.onMessage(runner.messages);
|
||||
}
|
||||
|
||||
private onMessage(message: IResultMessage | IResultMessage[]) {
|
||||
let hasError = false;
|
||||
let lines: number;
|
||||
if (isArray(message)) {
|
||||
hasError = message.find(e => e.isError) ? true : false;
|
||||
lines = message.reduce((currentTotal, resultMessage) => currentTotal + this.countMessageLines(resultMessage), 0);
|
||||
this.model.messages.push(...message);
|
||||
} else {
|
||||
hasError = message.isError;
|
||||
lines = this.countMessageLines(message);
|
||||
this.model.messages.push(message);
|
||||
}
|
||||
this.maximumBodySize += lines * 22;
|
||||
if (hasError) {
|
||||
this.setExpanded(true);
|
||||
}
|
||||
// convert to old VS Code tree interface with expandable methods
|
||||
let expandableTree: IExpandableTree = <IExpandableTree>this.tree;
|
||||
if (this.state.scrollPosition) {
|
||||
this.tree.refresh(this.model).then(() => {
|
||||
// Restore the previous scroll position when switching between tabs
|
||||
expandableTree.setScrollPosition(this.state.scrollPosition);
|
||||
});
|
||||
} else {
|
||||
const previousScrollPosition = expandableTree.getScrollPosition();
|
||||
this.tree.refresh(this.model).then(() => {
|
||||
// Scroll to the end if the user was already at the end otherwise leave the current scroll position
|
||||
if (previousScrollPosition === 1) {
|
||||
expandableTree.setScrollPosition(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private countMessageLines(resultMessage: IResultMessage): number {
|
||||
let lines = resultMessage.message.split('\n').length;
|
||||
this.messageLineCountMap.set(resultMessage, lines);
|
||||
return lines;
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.model.messages = [];
|
||||
this.model.totalExecuteMessage = undefined;
|
||||
this.tree.refresh(this.model);
|
||||
}
|
||||
|
||||
public set state(val: MessagePanelState) {
|
||||
this._state = val;
|
||||
// convert to old VS Code tree interface with expandable methods
|
||||
let expandableTree: IExpandableTree = <IExpandableTree>this.tree;
|
||||
if (this.state.scrollPosition) {
|
||||
expandableTree.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
this.setExpanded(!this.state.collapsed);
|
||||
}
|
||||
|
||||
public get state(): MessagePanelState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.queryRunnerDisposables);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class MessageDataSource implements IDataSource {
|
||||
getId(tree: ITree, element: Model | IResultMessageIntern): string {
|
||||
if (element instanceof Model) {
|
||||
return element.uuid;
|
||||
} else {
|
||||
if (!element.id) {
|
||||
element.id = generateUuid();
|
||||
}
|
||||
return element.id;
|
||||
}
|
||||
}
|
||||
|
||||
hasChildren(tree: ITree, element: any): boolean {
|
||||
return element instanceof Model;
|
||||
}
|
||||
|
||||
getChildren(tree: ITree, element: any): Promise<(IMessagePanelMessage | IMessagePanelBatchMessage)[]> {
|
||||
if (element instanceof Model) {
|
||||
let messages = element.messages;
|
||||
if (element.totalExecuteMessage) {
|
||||
messages = messages.concat(element.totalExecuteMessage);
|
||||
}
|
||||
return Promise.resolve(messages);
|
||||
} else {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
getParent(tree: ITree, element: any): Promise<void> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageRenderer implements IRenderer {
|
||||
constructor(private messageLineCountMap: Map<IResultMessage, number>) {
|
||||
}
|
||||
|
||||
getHeight(tree: ITree, element: any): number {
|
||||
const lineHeight = 22;
|
||||
if (this.messageLineCountMap.has(element)) {
|
||||
return lineHeight * this.messageLineCountMap.get(element);
|
||||
}
|
||||
return lineHeight;
|
||||
}
|
||||
|
||||
getTemplateId(tree: ITree, element: any): string {
|
||||
if (element instanceof Model) {
|
||||
return TemplateIds.MODEL;
|
||||
} else if (element.selection) {
|
||||
return TemplateIds.BATCH;
|
||||
} else if (element.isError) {
|
||||
return TemplateIds.ERROR;
|
||||
} else {
|
||||
return TemplateIds.MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IMessageTemplate | IBatchTemplate {
|
||||
|
||||
if (templateId === TemplateIds.MESSAGE) {
|
||||
container.append($('.time-stamp'));
|
||||
const message = $('.message');
|
||||
message.style.whiteSpace = 'pre';
|
||||
container.append(message);
|
||||
return { message };
|
||||
} else if (templateId === TemplateIds.BATCH) {
|
||||
const timeStamp = $('.time-stamp');
|
||||
container.append(timeStamp);
|
||||
const message = $('.batch-start');
|
||||
message.style.whiteSpace = 'pre';
|
||||
container.append(message);
|
||||
return { message, timeStamp };
|
||||
} else if (templateId === TemplateIds.ERROR) {
|
||||
container.append($('.time-stamp'));
|
||||
const message = $('.error-message');
|
||||
container.append(message);
|
||||
return { message };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
renderElement(tree: ITree, element: IResultMessage, templateId: string, templateData: IMessageTemplate | IBatchTemplate): void {
|
||||
if (templateId === TemplateIds.MESSAGE || templateId === TemplateIds.ERROR) {
|
||||
let data: IMessageTemplate = templateData;
|
||||
data.message.innerText = element.message;
|
||||
} else if (templateId === TemplateIds.BATCH) {
|
||||
let data = templateData as IBatchTemplate;
|
||||
data.timeStamp.innerText = element.time;
|
||||
data.message.innerText = element.message;
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageController extends WorkbenchTreeController {
|
||||
|
||||
private lastSelectedString: string = null;
|
||||
public toFocusOnClick: { focus(): void };
|
||||
|
||||
constructor(
|
||||
options: IControllerOptions,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IEditorService private workbenchEditorService: IEditorService,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
super(options, configurationService);
|
||||
}
|
||||
|
||||
protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
|
||||
const mouseEvent = <IMouseEvent>eventish;
|
||||
// input and output are one element in the tree => we only expand if the user clicked on the output.
|
||||
// if ((element.reference > 0 || (element instanceof RawObjectReplElement && element.hasChildren)) && mouseEvent.target.className.indexOf('input expression') === -1) {
|
||||
super.onLeftClick(tree, element, eventish, origin);
|
||||
tree.clearFocus();
|
||||
tree.deselect(element);
|
||||
// }
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection.type !== 'Range' || this.lastSelectedString === selection.toString()) {
|
||||
// only focus the input if the user is not currently selecting.
|
||||
this.toFocusOnClick.focus();
|
||||
}
|
||||
this.lastSelectedString = selection.toString();
|
||||
|
||||
if (element.selection) {
|
||||
let selection: ISelectionData = element.selection;
|
||||
// this is a batch statement
|
||||
let input = this.workbenchEditorService.activeEditor as QueryInput;
|
||||
input.updateSelection(selection);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Model {
|
||||
public messages: Array<IMessagePanelMessage | IMessagePanelBatchMessage> = [];
|
||||
public totalExecuteMessage: IMessagePanelMessage;
|
||||
|
||||
public uuid = generateUuid();
|
||||
|
||||
public focus() {
|
||||
|
||||
}
|
||||
}
|
||||
449
src/sql/workbench/parts/query/browser/query.contribution.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/media/overwriteVsIcons';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
|
||||
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
|
||||
import { QueryEditor } from 'sql/workbench/parts/query/browser/queryEditor';
|
||||
import { QueryResultsEditor } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||
import { QueryResultsInput } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import * as queryContext from 'sql/workbench/parts/query/common/queryContext';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { EditDataEditor } from 'sql/workbench/parts/editData/browser/editDataEditor';
|
||||
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
|
||||
import {
|
||||
RunQueryKeyboardAction, RunCurrentQueryKeyboardAction, CancelQueryKeyboardAction, RefreshIntellisenseKeyboardAction, ToggleQueryResultsKeyboardAction,
|
||||
RunQueryShortcutAction, RunCurrentQueryWithActualPlanKeyboardAction, FocusOnCurrentQueryKeyboardAction, ParseSyntaxAction
|
||||
} from 'sql/workbench/parts/query/browser/keyboardQueryActions';
|
||||
import * as gridActions from 'sql/workbench/parts/grid/views/gridActions';
|
||||
import * as gridCommands from 'sql/workbench/parts/grid/views/gridCommands';
|
||||
import { QueryPlanEditor } from 'sql/workbench/parts/queryPlan/electron-browser/queryPlanEditor';
|
||||
import { QueryPlanInput } from 'sql/workbench/parts/queryPlan/common/queryPlanInput';
|
||||
import * as Constants from 'sql/workbench/parts/query/common/constants';
|
||||
import { localize } from 'vs/nls';
|
||||
import { EditDataResultsEditor } from 'sql/workbench/parts/editData/browser/editDataResultsEditor';
|
||||
import { EditDataResultsInput } from 'sql/workbench/parts/editData/common/editDataResultsInput';
|
||||
|
||||
const gridCommandsWeightBonus = 100; // give our commands a little bit more weight over other default list/tree commands
|
||||
|
||||
export const QueryEditorVisibleCondition = ContextKeyExpr.has(queryContext.queryEditorVisibleId);
|
||||
export const ResultsGridFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(queryContext.resultsVisibleId), ContextKeyExpr.has(queryContext.resultsGridFocussedId));
|
||||
export const ResultsMessagesFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(queryContext.resultsVisibleId), ContextKeyExpr.has(queryContext.resultsMessagesFocussedId));
|
||||
|
||||
// Editor
|
||||
const queryResultsEditorDescriptor = new EditorDescriptor(
|
||||
QueryResultsEditor,
|
||||
QueryResultsEditor.ID,
|
||||
'QueryResults'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(queryResultsEditorDescriptor, [new SyncDescriptor(QueryResultsInput)]);
|
||||
|
||||
// Editor
|
||||
const queryEditorDescriptor = new EditorDescriptor(
|
||||
QueryEditor,
|
||||
QueryEditor.ID,
|
||||
'Query'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(queryEditorDescriptor, [new SyncDescriptor(QueryInput)]);
|
||||
|
||||
// Query Plan editor registration
|
||||
|
||||
const queryPlanEditorDescriptor = new EditorDescriptor(
|
||||
QueryPlanEditor,
|
||||
QueryPlanEditor.ID,
|
||||
'QueryPlan'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(queryPlanEditorDescriptor, [new SyncDescriptor(QueryPlanInput)]);
|
||||
|
||||
// Editor
|
||||
const editDataEditorDescriptor = new EditorDescriptor(
|
||||
EditDataEditor,
|
||||
EditDataEditor.ID,
|
||||
'EditData'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(editDataEditorDescriptor, [new SyncDescriptor(EditDataInput)]);
|
||||
|
||||
// Editor
|
||||
const editDataResultsEditorDescriptor = new EditorDescriptor(
|
||||
EditDataResultsEditor,
|
||||
EditDataResultsEditor.ID,
|
||||
'EditDataResults'
|
||||
);
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
|
||||
.registerEditor(editDataResultsEditorDescriptor, [new SyncDescriptor(EditDataResultsInput)]);
|
||||
|
||||
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
|
||||
|
||||
// Query Actions
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
RunQueryKeyboardAction,
|
||||
RunQueryKeyboardAction.ID,
|
||||
RunQueryKeyboardAction.LABEL,
|
||||
{ primary: KeyCode.F5 }
|
||||
),
|
||||
RunQueryKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
// Touch Bar
|
||||
if (isMacintosh) {
|
||||
// Only show Run Query if the active editor is a query editor.
|
||||
MenuRegistry.appendMenuItem(MenuId.TouchBarContext, {
|
||||
command: { id: RunQueryKeyboardAction.ID, title: RunQueryKeyboardAction.LABEL },
|
||||
group: 'query',
|
||||
when: new ContextKeyEqualsExpr('activeEditor', 'workbench.editor.queryEditor')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
RunCurrentQueryKeyboardAction,
|
||||
RunCurrentQueryKeyboardAction.ID,
|
||||
RunCurrentQueryKeyboardAction.LABEL,
|
||||
{ primary: KeyMod.CtrlCmd | KeyCode.F5 }
|
||||
),
|
||||
RunCurrentQueryKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
RunCurrentQueryWithActualPlanKeyboardAction,
|
||||
RunCurrentQueryWithActualPlanKeyboardAction.ID,
|
||||
RunCurrentQueryWithActualPlanKeyboardAction.LABEL,
|
||||
{ primary: KeyMod.CtrlCmd | KeyCode.KEY_M }
|
||||
),
|
||||
RunCurrentQueryWithActualPlanKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
CancelQueryKeyboardAction,
|
||||
CancelQueryKeyboardAction.ID,
|
||||
CancelQueryKeyboardAction.LABEL,
|
||||
{ primary: KeyMod.Alt | KeyCode.PauseBreak }
|
||||
),
|
||||
CancelQueryKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
RefreshIntellisenseKeyboardAction,
|
||||
RefreshIntellisenseKeyboardAction.ID,
|
||||
RefreshIntellisenseKeyboardAction.LABEL
|
||||
),
|
||||
RefreshIntellisenseKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
FocusOnCurrentQueryKeyboardAction,
|
||||
FocusOnCurrentQueryKeyboardAction.ID,
|
||||
FocusOnCurrentQueryKeyboardAction.LABEL,
|
||||
{ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O }
|
||||
),
|
||||
FocusOnCurrentQueryKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
ParseSyntaxAction,
|
||||
ParseSyntaxAction.ID,
|
||||
ParseSyntaxAction.LABEL
|
||||
),
|
||||
ParseSyntaxAction.LABEL
|
||||
);
|
||||
|
||||
// Grid actions
|
||||
|
||||
actionRegistry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
ToggleQueryResultsKeyboardAction,
|
||||
ToggleQueryResultsKeyboardAction.ID,
|
||||
ToggleQueryResultsKeyboardAction.LABEL,
|
||||
{ primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R },
|
||||
QueryEditorVisibleCondition
|
||||
),
|
||||
ToggleQueryResultsKeyboardAction.LABEL
|
||||
);
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_COPY_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
handler: gridCommands.copySelection
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.MESSAGES_SELECTALL_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsMessagesFocusCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
|
||||
handler: gridCommands.selectAllMessages
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_SELECTALL_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
|
||||
handler: gridCommands.selectAll
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.MESSAGES_COPY_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsMessagesFocusCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
handler: gridCommands.copyMessagesSelection
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_SAVECSV_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_C),
|
||||
handler: gridCommands.saveAsCsv
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_SAVEJSON_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_J),
|
||||
handler: gridCommands.saveAsJson
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_SAVEEXCEL_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_E),
|
||||
handler: gridCommands.saveAsExcel
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_SAVEXML_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_X),
|
||||
handler: gridCommands.saveAsXml
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_VIEWASCHART_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_V),
|
||||
handler: gridCommands.viewAsChart
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GRID_GOTONEXTGRID_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ResultsGridFocusCondition,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_R, KeyMod.CtrlCmd | KeyCode.KEY_N),
|
||||
handler: gridCommands.goToNextGrid
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.TOGGLERESULTS_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: QueryEditorVisibleCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R,
|
||||
handler: gridCommands.toggleResultsPane
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.TOGGLEMESSAGES_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: QueryEditorVisibleCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y,
|
||||
handler: gridCommands.toggleMessagePane
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: gridActions.GOTONEXTQUERYOUTPUTTAB_ID,
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: QueryEditorVisibleCondition,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_P,
|
||||
handler: gridCommands.goToNextQueryOutputTab
|
||||
});
|
||||
|
||||
// Intellisense and other configuration options
|
||||
let registryProperties = {
|
||||
'sql.messagesDefaultOpen': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.messagesDefaultOpen', 'True for the messages pane to be open by default; false for closed'),
|
||||
'default': true
|
||||
},
|
||||
'sql.saveAsCsv.includeHeaders': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.saveAsCsv.includeHeaders', '[Optional] When true, column headers are included when saving results as CSV'),
|
||||
'default': true
|
||||
},
|
||||
'sql.saveAsCsv.delimiter': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.saveAsCsv.delimiter', '[Optional] The custom delimiter to use between values when saving as CSV'),
|
||||
'default': ','
|
||||
},
|
||||
'sql.saveAsCsv.lineSeperator': {
|
||||
'type': '',
|
||||
'description': localize('sql.saveAsCsv.lineSeperator', '[Optional] Character(s) used for seperating rows when saving results as CSV'),
|
||||
'default': null
|
||||
},
|
||||
'sql.saveAsCsv.textIdentifier': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.saveAsCsv.textIdentifier', '[Optional] Character used for enclosing text fields when saving results as CSV'),
|
||||
'default': '\"'
|
||||
},
|
||||
'sql.saveAsCsv.encoding': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.saveAsCsv.encoding', '[Optional] File encoding used when saving results as CSV'),
|
||||
'default': 'utf-8'
|
||||
},
|
||||
'sql.results.streaming': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.results.streaming', 'Enable results streaming; contains few minor visual issues'),
|
||||
'default': true
|
||||
},
|
||||
'sql.saveAsXml.formatted': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.saveAsXml.formatted', '[Optional] When true, XML output will be formatted when saving results as XML'),
|
||||
'default': true
|
||||
},
|
||||
'sql.saveAsXml.encoding': {
|
||||
'type': 'string',
|
||||
'description': localize('sql.saveAsXml.encoding', '[Optional] File encoding used when saving results as XML'),
|
||||
'default': 'utf-8'
|
||||
},
|
||||
'sql.copyIncludeHeaders': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.copyIncludeHeaders', '[Optional] Configuration options for copying results from the Results View'),
|
||||
'default': false
|
||||
},
|
||||
'sql.copyRemoveNewLine': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.copyRemoveNewLine', '[Optional] Configuration options for copying multi-line results from the Results View'),
|
||||
'default': true
|
||||
},
|
||||
'sql.showBatchTime': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.showBatchTime', '[Optional] Should execution time be shown for individual batches'),
|
||||
'default': false
|
||||
},
|
||||
'sql.chart.defaultChartType': {
|
||||
'enum': Constants.allChartTypes,
|
||||
'default': Constants.chartTypeHorizontalBar,
|
||||
'description': localize('defaultChartType', "[Optional] the default chart type to use when opening Chart Viewer from a Query Results")
|
||||
},
|
||||
'sql.tabColorMode': {
|
||||
'type': 'string',
|
||||
'enum': [Constants.tabColorModeOff, Constants.tabColorModeBorder, Constants.tabColorModeFill],
|
||||
'enumDescriptions': [
|
||||
localize('tabColorMode.off', "Tab coloring will be disabled"),
|
||||
localize('tabColorMode.border', "The top border of each editor tab will be colored to match the relevant server group"),
|
||||
localize('tabColorMode.fill', "Each editor tab's background color will match the relevant server group"),
|
||||
],
|
||||
'default': Constants.tabColorModeOff,
|
||||
'description': localize('tabColorMode', "Controls how to color tabs based on the server group of their active connection")
|
||||
},
|
||||
'sql.showConnectionInfoInTitle': {
|
||||
'type': 'boolean',
|
||||
'description': localize('showConnectionInfoInTitle', "Controls whether to show the connection info for a tab in the title."),
|
||||
'default': true
|
||||
},
|
||||
'sql.promptToSaveGeneratedFiles': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': localize('sql.promptToSaveGeneratedFiles', 'Prompt to save generated SQL files')
|
||||
},
|
||||
'mssql.intelliSense.enableIntelliSense': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('mssql.intelliSense.enableIntelliSense', 'Should IntelliSense be enabled')
|
||||
},
|
||||
'mssql.intelliSense.enableErrorChecking': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('mssql.intelliSense.enableErrorChecking', 'Should IntelliSense error checking be enabled')
|
||||
},
|
||||
'mssql.intelliSense.enableSuggestions': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('mssql.intelliSense.enableSuggestions', 'Should IntelliSense suggestions be enabled')
|
||||
},
|
||||
'mssql.intelliSense.enableQuickInfo': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'description': localize('mssql.intelliSense.enableQuickInfo', 'Should IntelliSense quick info be enabled')
|
||||
},
|
||||
'mssql.intelliSense.lowerCaseSuggestions': {
|
||||
'type': 'boolean',
|
||||
'default': false,
|
||||
'description': localize('mssql.intelliSense.lowerCaseSuggestions', 'Should IntelliSense suggestions be lowercase')
|
||||
}
|
||||
};
|
||||
|
||||
// Setup keybindings
|
||||
let initialShortcuts = [
|
||||
{ name: 'sp_help', primary: KeyMod.Alt + KeyCode.F2 },
|
||||
// Note: using Ctrl+Shift+N since Ctrl+N is used for "open editor at index" by default. This means it's different from SSMS
|
||||
{ name: 'sp_who', primary: KeyMod.WinCtrl + KeyMod.Shift + KeyCode.KEY_1 },
|
||||
{ name: 'sp_lock', primary: KeyMod.WinCtrl + KeyMod.Shift + KeyCode.KEY_2 }
|
||||
];
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const queryIndex = i + 1;
|
||||
let settingKey = `sql.query.shortcut${queryIndex}`;
|
||||
let defaultVal = i < initialShortcuts.length ? initialShortcuts[i].name : '';
|
||||
let defaultPrimary = i < initialShortcuts.length ? initialShortcuts[i].primary : null;
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: `workbench.action.query.shortcut${queryIndex}`,
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
when: QueryEditorVisibleCondition,
|
||||
primary: defaultPrimary,
|
||||
handler: accessor => {
|
||||
accessor.get(IInstantiationService).createInstance(RunQueryShortcutAction).run(queryIndex);
|
||||
}
|
||||
});
|
||||
registryProperties[settingKey] = {
|
||||
'type': 'string',
|
||||
'default': defaultVal,
|
||||
'description': localize('queryShortcutDescription',
|
||||
'Set keybinding workbench.action.query.shortcut{0} to run the shortcut text as a procedure call. Any selected text in the query editor will be passed as a parameter',
|
||||
queryIndex)
|
||||
};
|
||||
}
|
||||
|
||||
// Register the query-related configuration options
|
||||
let configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'sqlEditor',
|
||||
'title': 'SQL Editor',
|
||||
'type': 'object',
|
||||
'properties': registryProperties
|
||||
});
|
||||
660
src/sql/workbench/parts/query/browser/queryActions.ts
Normal file
@@ -0,0 +1,660 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/queryActions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action, IActionItem, IActionRunner } from 'vs/base/common/actions';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { append, $ } from 'vs/base/browser/dom';
|
||||
|
||||
import { ISelectionData } from 'azdata';
|
||||
import {
|
||||
IConnectionManagementService,
|
||||
IConnectionParams,
|
||||
INewConnectionParams,
|
||||
ConnectionType,
|
||||
RunQueryOnConnectionMode
|
||||
} from 'sql/platform/connection/common/connectionManagement';
|
||||
import { QueryEditor } from 'sql/workbench/parts/query/browser/queryEditor';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { attachEditableDropdownStyler, attachSelectBoxStyler } from 'sql/platform/theme/common/styler';
|
||||
import { EventEmitter } from 'sql/base/common/eventEmitter';
|
||||
import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
|
||||
/**
|
||||
* Action class that query-based Actions will extend. This base class automatically handles activating and
|
||||
* deactivating the button when a SQL file is opened.
|
||||
*/
|
||||
export abstract class QueryTaskbarAction extends Action {
|
||||
|
||||
private _classes: string[];
|
||||
|
||||
constructor(
|
||||
protected _connectionManagementService: IConnectionManagementService,
|
||||
protected editor: QueryEditor,
|
||||
id: string,
|
||||
enabledClass: string
|
||||
) {
|
||||
super(id);
|
||||
this.enabled = true;
|
||||
this._setCssClass(enabledClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is executed when the button is clicked.
|
||||
*/
|
||||
public abstract run(): Promise<void>;
|
||||
|
||||
protected updateCssClass(enabledClass: string): void {
|
||||
// set the class, useful on change of label or icon
|
||||
this._setCssClass(enabledClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CSS classes combining the parent and child classes.
|
||||
* Public for testing only.
|
||||
*/
|
||||
private _setCssClass(enabledClass: string): void {
|
||||
this._classes = [];
|
||||
|
||||
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 for testing only.
|
||||
*/
|
||||
public isConnected(editor: QueryEditor): boolean {
|
||||
if (!editor || !editor.currentQueryInput) {
|
||||
return false;
|
||||
}
|
||||
return this._connectionManagementService.isConnected(editor.currentQueryInput.uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the given editor to it's current URI.
|
||||
* Public for testing only.
|
||||
*/
|
||||
protected connectEditor(editor: QueryEditor, runQueryOnCompletion?: RunQueryOnConnectionMode, selection?: ISelectionData): void {
|
||||
let params: INewConnectionParams = {
|
||||
input: editor.currentQueryInput,
|
||||
connectionType: ConnectionType.editor,
|
||||
runQueryOnCompletion: runQueryOnCompletion ? runQueryOnCompletion : RunQueryOnConnectionMode.none,
|
||||
querySelection: selection
|
||||
};
|
||||
this._connectionManagementService.showConnectionDialog(params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that runs a query in the active SQL text document.
|
||||
*/
|
||||
export class RunQueryAction extends QueryTaskbarAction {
|
||||
|
||||
public static EnabledClass = 'start';
|
||||
public static ID = 'runQueryAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
@IQueryModelService protected _queryModelService: IQueryModelService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(connectionManagementService, editor, RunQueryAction.ID, RunQueryAction.EnabledClass);
|
||||
this.label = nls.localize('runQueryLabel', 'Run');
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
if (!this.editor.isSelectionEmpty()) {
|
||||
if (this.isConnected(this.editor)) {
|
||||
// If we are already connected, run the query
|
||||
this.runQuery(this.editor);
|
||||
} else {
|
||||
// If we are not already connected, prompt for connection and run the query if the
|
||||
// connection succeeds. "runQueryOnCompletion=true" will cause the query to run after connection
|
||||
this.connectEditor(this.editor, RunQueryOnConnectionMode.executeQuery, this.editor.getSelection());
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public runCurrent(): Promise<void> {
|
||||
if (!this.editor.isSelectionEmpty()) {
|
||||
if (this.isConnected(this.editor)) {
|
||||
// If we are already connected, run the query
|
||||
this.runQuery(this.editor, true);
|
||||
} else {
|
||||
// If we are not already connected, prompt for connection and run the query if the
|
||||
// connection succeeds. "runQueryOnCompletion=true" will cause the query to run after connection
|
||||
this.connectEditor(this.editor, RunQueryOnConnectionMode.executeCurrentQuery, this.editor.getSelection(false));
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public runQuery(editor: QueryEditor, runCurrentStatement: boolean = false) {
|
||||
if (!editor) {
|
||||
editor = this.editor;
|
||||
}
|
||||
|
||||
if (this.isConnected(editor)) {
|
||||
// if the selection isn't empty then execute the selection
|
||||
// otherwise, either run the statement or the script depending on parameter
|
||||
let selection: ISelectionData = editor.getSelection(false);
|
||||
if (runCurrentStatement && selection && this.isCursorPosition(selection)) {
|
||||
editor.currentQueryInput.runQueryStatement(selection);
|
||||
} else {
|
||||
// get the selection again this time with trimming
|
||||
selection = editor.getSelection();
|
||||
editor.currentQueryInput.runQuery(selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected isCursorPosition(selection: ISelectionData) {
|
||||
return selection.startLine === selection.endLine
|
||||
&& selection.startColumn === selection.endColumn;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that cancels the running query in the current SQL text document.
|
||||
*/
|
||||
export class CancelQueryAction extends QueryTaskbarAction {
|
||||
|
||||
public static EnabledClass = 'stop';
|
||||
public static ID = 'cancelQueryAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(connectionManagementService, editor, CancelQueryAction.ID, CancelQueryAction.EnabledClass);
|
||||
this.enabled = false;
|
||||
this.label = nls.localize('cancelQueryLabel', 'Cancel');
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
if (this.isConnected(this.editor)) {
|
||||
this._queryModelService.cancelQuery(this.editor.currentQueryInput.uri);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that runs a query in the active SQL text document.
|
||||
*/
|
||||
export class EstimatedQueryPlanAction extends QueryTaskbarAction {
|
||||
|
||||
public static EnabledClass = 'estimatedQueryPlan';
|
||||
public static ID = 'estimatedQueryPlanAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(connectionManagementService, editor, EstimatedQueryPlanAction.ID, EstimatedQueryPlanAction.EnabledClass);
|
||||
this.label = nls.localize('estimatedQueryPlan', 'Explain');
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
if (!this.editor.isSelectionEmpty()) {
|
||||
if (this.isConnected(this.editor)) {
|
||||
// If we are already connected, run the query
|
||||
this.runQuery(this.editor);
|
||||
} else {
|
||||
// If we are not already connected, prompt for connection and run the query if the
|
||||
// connection succeeds. "runQueryOnCompletion=true" will cause the query to run after connection
|
||||
this.connectEditor(this.editor, RunQueryOnConnectionMode.estimatedQueryPlan, this.editor.getSelection());
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public runQuery(editor: QueryEditor) {
|
||||
if (!editor) {
|
||||
editor = this.editor;
|
||||
}
|
||||
|
||||
if (this.isConnected(editor)) {
|
||||
editor.currentQueryInput.runQuery(editor.getSelection(), {
|
||||
displayEstimatedQueryPlan: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ActualQueryPlanAction extends QueryTaskbarAction {
|
||||
public static EnabledClass = 'actualQueryPlan';
|
||||
public static ID = 'actualQueryPlanAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(connectionManagementService, editor, ActualQueryPlanAction.ID, ActualQueryPlanAction.EnabledClass);
|
||||
this.label = nls.localize('actualQueryPlan', "Actual");
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
if (!this.editor.isSelectionEmpty()) {
|
||||
if (this.isConnected(this.editor)) {
|
||||
// If we are already connected, run the query
|
||||
this.runQuery(this.editor);
|
||||
} else {
|
||||
// If we are not already connected, prompt for connection and run the query if the
|
||||
// connection succeeds. "runQueryOnCompletion=true" will cause the query to run after connection
|
||||
this.connectEditor(this.editor, RunQueryOnConnectionMode.actualQueryPlan, this.editor.getSelection());
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public runQuery(editor: QueryEditor) {
|
||||
if (!editor) {
|
||||
editor = this.editor;
|
||||
}
|
||||
|
||||
if (this.isConnected(editor)) {
|
||||
let selection = editor.getSelection();
|
||||
if (!selection) {
|
||||
selection = editor.getAllSelection();
|
||||
}
|
||||
editor.currentQueryInput.runQuery(selection, {
|
||||
displayActualQueryPlan: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that disconnects the connection associated with the current query file.
|
||||
*/
|
||||
export class DisconnectDatabaseAction extends QueryTaskbarAction {
|
||||
|
||||
public static EnabledClass = 'disconnect';
|
||||
public static ID = 'disconnectDatabaseAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(connectionManagementService, editor, DisconnectDatabaseAction.ID, DisconnectDatabaseAction.EnabledClass);
|
||||
this.label = nls.localize('disconnectDatabaseLabel', 'Disconnect');
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
// Call disconnectEditor regardless of the connection state and let the ConnectionManagementService
|
||||
// determine if we need to disconnect, cancel an in-progress conneciton, or do nothing
|
||||
this._connectionManagementService.disconnectEditor(this.editor.currentQueryInput);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that launches a connection dialogue for the current query file
|
||||
*/
|
||||
export class ConnectDatabaseAction extends QueryTaskbarAction {
|
||||
|
||||
public static EnabledDefaultClass = 'connect';
|
||||
public static EnabledChangeClass = 'changeConnection';
|
||||
public static ID = 'connectDatabaseAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
isChangeConnectionAction: boolean,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
let label: string;
|
||||
let enabledClass: string;
|
||||
|
||||
if (isChangeConnectionAction) {
|
||||
enabledClass = ConnectDatabaseAction.EnabledChangeClass;
|
||||
label = nls.localize('changeConnectionDatabaseLabel', 'Change Connection');
|
||||
} else {
|
||||
enabledClass = ConnectDatabaseAction.EnabledDefaultClass;
|
||||
label = nls.localize('connectDatabaseLabel', 'Connect');
|
||||
}
|
||||
|
||||
super(connectionManagementService, editor, ConnectDatabaseAction.ID, enabledClass);
|
||||
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
this.connectEditor(this.editor);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that either launches a connection dialogue for the current query file,
|
||||
* or disconnects the active connection
|
||||
*/
|
||||
export class ToggleConnectDatabaseAction extends QueryTaskbarAction {
|
||||
|
||||
public static ConnectClass = 'connect';
|
||||
public static DisconnectClass = 'disconnect';
|
||||
public static ID = 'toggleConnectDatabaseAction';
|
||||
|
||||
private _connected: boolean;
|
||||
private _connectLabel: string;
|
||||
private _disconnectLabel: string;
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
isConnected: boolean,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
let enabledClass: string;
|
||||
|
||||
super(connectionManagementService, editor, ToggleConnectDatabaseAction.ID, enabledClass);
|
||||
|
||||
this._connectLabel = nls.localize('connectDatabaseLabel', 'Connect');
|
||||
this._disconnectLabel = nls.localize('disconnectDatabaseLabel', 'Disconnect');
|
||||
|
||||
this.connected = isConnected;
|
||||
}
|
||||
|
||||
public get connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
public set connected(value: boolean) {
|
||||
// intentionally always updating, since parent class handles skipping if values
|
||||
this._connected = value;
|
||||
this.updateLabelAndIcon();
|
||||
}
|
||||
|
||||
private updateLabelAndIcon(): void {
|
||||
if (this._connected) {
|
||||
// We are connected, so show option to disconnect
|
||||
this.label = this._disconnectLabel;
|
||||
this.updateCssClass(ToggleConnectDatabaseAction.DisconnectClass);
|
||||
} else {
|
||||
this.label = this._connectLabel;
|
||||
this.updateCssClass(ToggleConnectDatabaseAction.ConnectClass);
|
||||
}
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
if (this.connected) {
|
||||
// Call disconnectEditor regardless of the connection state and let the ConnectionManagementService
|
||||
// determine if we need to disconnect, cancel an in-progress connection, or do nothing
|
||||
this._connectionManagementService.disconnectEditor(this.editor.currentQueryInput);
|
||||
} else {
|
||||
this.connectEditor(this.editor);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action class that is tied with ListDatabasesActionItem.
|
||||
*/
|
||||
export class ListDatabasesAction extends QueryTaskbarAction {
|
||||
|
||||
public static EnabledClass = '';
|
||||
public static ID = 'listDatabaseQueryAction';
|
||||
|
||||
constructor(
|
||||
editor: QueryEditor,
|
||||
@IConnectionManagementService connectionManagementService: IConnectionManagementService
|
||||
) {
|
||||
super(connectionManagementService, editor, ListDatabasesAction.ID, undefined);
|
||||
this.enabled = false;
|
||||
this.class = ListDatabasesAction.EnabledClass;
|
||||
}
|
||||
|
||||
public run(): Promise<void> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Action item that handles the dropdown (combobox) that lists the available databases.
|
||||
* Based off StartDebugActionItem.
|
||||
*/
|
||||
export class ListDatabasesActionItem extends EventEmitter implements IActionItem {
|
||||
public static ID = 'listDatabaseQueryActionItem';
|
||||
|
||||
public actionRunner: IActionRunner;
|
||||
private _toDispose: IDisposable[];
|
||||
private _context: any;
|
||||
private _currentDatabaseName: string;
|
||||
private _isConnected: boolean;
|
||||
private _databaseListDropdown: HTMLElement;
|
||||
private _dropdown: Dropdown;
|
||||
private _databaseSelectBox: SelectBox;
|
||||
private _isInAccessibilityMode: boolean;
|
||||
private readonly _selectDatabaseString: string = nls.localize("selectDatabase", "Select Database");
|
||||
|
||||
// CONSTRUCTOR /////////////////////////////////////////////////////////
|
||||
constructor(
|
||||
private _editor: QueryEditor,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IContextViewService contextViewProvider: IContextViewService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@ILayoutService layoutService: ILayoutService
|
||||
) {
|
||||
super();
|
||||
this._toDispose = [];
|
||||
this._databaseListDropdown = $('.databaseListDropdown');
|
||||
this._isInAccessibilityMode = this._configurationService.getValue('editor.accessibilitySupport') === 'on';
|
||||
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox = new SelectBox([this._selectDatabaseString], this._selectDatabaseString, contextViewProvider, undefined, { ariaLabel: this._selectDatabaseString });
|
||||
this._databaseSelectBox.render(this._databaseListDropdown);
|
||||
this._databaseSelectBox.onDidSelect(e => { this.databaseSelected(e.selected); });
|
||||
this._databaseSelectBox.disable();
|
||||
|
||||
} else {
|
||||
this._dropdown = new Dropdown(this._databaseListDropdown, contextViewProvider, layoutService, {
|
||||
strictSelection: true,
|
||||
placeholder: this._selectDatabaseString,
|
||||
ariaLabel: this._selectDatabaseString,
|
||||
actionLabel: nls.localize('listDatabases.toggleDatabaseNameDropdown', 'Select Database Toggle Dropdown')
|
||||
});
|
||||
this._dropdown.onValueChange(s => this.databaseSelected(s));
|
||||
this._toDispose.push(this._dropdown.onFocus(() => { self.onDropdownFocus(); }));
|
||||
}
|
||||
|
||||
// Register event handlers
|
||||
let self = this;
|
||||
this._toDispose.push(this._connectionManagementService.onConnectionChanged(params => { self.onConnectionChanged(params); }));
|
||||
}
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
public render(container: HTMLElement): void {
|
||||
append(container, this._databaseListDropdown);
|
||||
}
|
||||
|
||||
public style(styles) {
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox.style(styles);
|
||||
}
|
||||
else {
|
||||
this._dropdown.style(styles);
|
||||
}
|
||||
}
|
||||
|
||||
public setActionContext(context: any): void {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return !!this._isConnected;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox.focus();
|
||||
} else {
|
||||
this._dropdown.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox.blur();
|
||||
} else {
|
||||
this._dropdown.blur();
|
||||
}
|
||||
}
|
||||
|
||||
public attachStyler(themeService: IThemeService): IDisposable {
|
||||
if (this._isInAccessibilityMode) {
|
||||
return attachSelectBoxStyler(this, themeService);
|
||||
} else {
|
||||
return attachEditableDropdownStyler(this, themeService);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
}
|
||||
|
||||
// EVENT HANDLERS FROM EDITOR //////////////////////////////////////////
|
||||
public onConnected(): void {
|
||||
let dbName = this.getCurrentDatabaseName();
|
||||
this.updateConnection(dbName);
|
||||
}
|
||||
|
||||
public onDisconnect(): void {
|
||||
this._isConnected = false;
|
||||
this._currentDatabaseName = undefined;
|
||||
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox.disable();
|
||||
this._databaseSelectBox.setOptions([this._selectDatabaseString]);
|
||||
} else {
|
||||
this._dropdown.enabled = false;
|
||||
this._dropdown.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private databaseSelected(dbName: string): void {
|
||||
let uri = this._editor.connectedUri;
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
let profile = this._connectionManagementService.getConnectionProfile(uri);
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._connectionManagementService.changeDatabase(this._editor.uri, dbName)
|
||||
.then(
|
||||
result => {
|
||||
if (!result) {
|
||||
this.resetDatabaseName();
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('changeDatabase.failed', "Failed to change database")
|
||||
});
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.resetDatabaseName();
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('changeDatabase.failedWithError', "Failed to change database {0}", error)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getCurrentDatabaseName() {
|
||||
let uri = this._editor.connectedUri;
|
||||
if (uri) {
|
||||
let profile = this._connectionManagementService.getConnectionProfile(uri);
|
||||
if (profile) {
|
||||
return profile.databaseName;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private resetDatabaseName() {
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox.selectWithOptionName(this.getCurrentDatabaseName());
|
||||
} else {
|
||||
this._dropdown.value = this.getCurrentDatabaseName();
|
||||
}
|
||||
}
|
||||
|
||||
private onConnectionChanged(connParams: IConnectionParams): void {
|
||||
if (!connParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = this._editor.connectedUri;
|
||||
if (uri !== connParams.connectionUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateConnection(connParams.connectionProfile.databaseName);
|
||||
}
|
||||
|
||||
private onDropdownFocus(): void {
|
||||
let self = this;
|
||||
|
||||
let uri = self._editor.connectedUri;
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._connectionManagementService.listDatabases(uri)
|
||||
.then(result => {
|
||||
if (result && result.databaseNames) {
|
||||
this._dropdown.values = result.databaseNames;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateConnection(databaseName: string) {
|
||||
this._isConnected = true;
|
||||
this._currentDatabaseName = databaseName;
|
||||
|
||||
if (this._isInAccessibilityMode) {
|
||||
this._databaseSelectBox.enable();
|
||||
let self = this;
|
||||
let uri = self._editor.connectedUri;
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
self._connectionManagementService.listDatabases(uri)
|
||||
.then(result => {
|
||||
if (result && result.databaseNames) {
|
||||
this._databaseSelectBox.setOptions(result.databaseNames);
|
||||
}
|
||||
this._databaseSelectBox.selectWithOptionName(databaseName);
|
||||
});
|
||||
} else {
|
||||
this._dropdown.enabled = true;
|
||||
this._dropdown.value = databaseName;
|
||||
}
|
||||
}
|
||||
|
||||
// TESTING PROPERTIES //////////////////////////////////////////////////
|
||||
public get currentDatabaseName(): string {
|
||||
return this._currentDatabaseName;
|
||||
}
|
||||
|
||||
}
|
||||
973
src/sql/workbench/parts/query/browser/queryEditor.ts
Normal file
@@ -0,0 +1,973 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/queryEditor';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
|
||||
import { EditorInput, EditorOptions, IEditorControl, IEditor, TextEditorOptions } from 'vs/workbench/common/editor';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { VerticalFlexibleSash, HorizontalFlexibleSash, IFlexibleSash } from 'sql/workbench/parts/query/browser/flexibleSash';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
|
||||
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 { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor';
|
||||
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ISelectionData } from 'azdata';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
|
||||
import { QueryResultsInput } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { QueryResultsEditor } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||
import * as queryContext from 'sql/workbench/parts/query/common/queryContext';
|
||||
import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import {
|
||||
RunQueryAction, CancelQueryAction, ListDatabasesAction, ListDatabasesActionItem,
|
||||
ConnectDatabaseAction, ToggleConnectDatabaseAction, EstimatedQueryPlanAction,
|
||||
ActualQueryPlanAction
|
||||
} from 'sql/workbench/parts/query/browser/queryActions';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import { IEditorDescriptorService } from 'sql/workbench/services/queryEditor/common/editorDescriptorService';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
|
||||
/**
|
||||
* Editor that hosts 2 sub-editors: A TextResourceEditor for SQL file editing, and a QueryResultsEditor
|
||||
* for viewing and editing query results. This editor is based off SideBySideEditor.
|
||||
*/
|
||||
export class QueryEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.queryEditor';
|
||||
|
||||
// The height of the tabs above the editor
|
||||
private readonly _tabHeight: number = 35;
|
||||
|
||||
// The minimum width/height of the editors hosted in the QueryEditor
|
||||
private readonly _minEditorSize: number = 100;
|
||||
|
||||
private _sash: IFlexibleSash;
|
||||
private _editorTopOffset: number;
|
||||
private _orientation: Orientation;
|
||||
private _dimension: DOM.Dimension;
|
||||
|
||||
private _resultsEditor: QueryResultsEditor;
|
||||
private _resultsEditorContainer: HTMLElement;
|
||||
|
||||
private _sqlEditor: TextResourceEditor;
|
||||
private _sqlEditorContainer: HTMLElement;
|
||||
|
||||
private _taskbar: Taskbar;
|
||||
private _taskbarContainer: HTMLElement;
|
||||
private _listDatabasesActionItem: ListDatabasesActionItem;
|
||||
|
||||
|
||||
private queryEditorVisible: IContextKey<boolean>;
|
||||
|
||||
private _runQueryAction: RunQueryAction;
|
||||
private _cancelQueryAction: CancelQueryAction;
|
||||
private _toggleConnectDatabaseAction: ToggleConnectDatabaseAction;
|
||||
private _changeConnectionAction: ConnectDatabaseAction;
|
||||
private _listDatabasesAction: ListDatabasesAction;
|
||||
private _estimatedQueryPlanAction: EstimatedQueryPlanAction;
|
||||
private _actualQueryPlanAction: ActualQueryPlanAction;
|
||||
|
||||
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,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(QueryEditor.ID, _telemetryService, themeService, storageService);
|
||||
|
||||
this._orientation = Orientation.HORIZONTAL;
|
||||
|
||||
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 {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// PROPERTIES //////////////////////////////////////////////////////////
|
||||
/**
|
||||
* Returns the URI of this editor if it is connected.
|
||||
* @returns URI of the editor if connected, undefined otherwise
|
||||
*/
|
||||
public get connectedUri(): string {
|
||||
return this._connectionManagementService.isConnected(this.uri)
|
||||
? this.uri
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI of this editor if an input is associated with it
|
||||
* @return URI of this if input is associated, undefined otherwise
|
||||
*/
|
||||
get uri(): string {
|
||||
let input: QueryInput = <QueryInput>this.input;
|
||||
return input
|
||||
? input.getQueryResultsInputResource()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// PUBLIC METHODS ////////////////////////////////////////////////////////////
|
||||
public get currentQueryInput(): QueryInput {
|
||||
return <QueryInput>this.input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to create the editor in the parent element.
|
||||
*/
|
||||
public createEditor(parent: HTMLElement): void {
|
||||
const parentElement = parent;
|
||||
DOM.addClass(parentElement, 'side-by-side-editor');
|
||||
this._createTaskbar(parentElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input data for this editor.
|
||||
*/
|
||||
public setInput(newInput: QueryInput, options?: EditorOptions): Promise<void> {
|
||||
const oldInput = <QueryInput>this.input;
|
||||
|
||||
if (newInput.matches(oldInput)) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Make sure all event callbacks will be sent to this QueryEditor in the case that this QueryInput was moved from
|
||||
// another QueryEditor
|
||||
let taskbarCallback: IDisposable = newInput.updateTaskbarEvent(() => this._updateTaskbar());
|
||||
let showResultsCallback: IDisposable = newInput.showQueryResultsEditorEvent(() => this._showQueryResultsEditor());
|
||||
let selectionCallback: IDisposable = newInput.updateSelectionEvent((selection) => this._setSelection(selection));
|
||||
newInput.setEventCallbacks([taskbarCallback, showResultsCallback, selectionCallback]);
|
||||
|
||||
return super.setInput(newInput, options, CancellationToken.None)
|
||||
.then(() => this._updateInput(oldInput, newInput, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this editor and the 2 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);
|
||||
}
|
||||
|
||||
|
||||
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() === QueryEditor.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
|
||||
*/
|
||||
public focus(): void {
|
||||
if (this._sqlEditor) {
|
||||
this._sqlEditor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the editor control for the text editor.
|
||||
*/
|
||||
public getControl(): IEditorControl {
|
||||
if (this._sqlEditor) {
|
||||
return this._sqlEditor.getControl();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getQueryResultsEditor(): QueryResultsEditor {
|
||||
return this._resultsEditor;
|
||||
}
|
||||
|
||||
public getSqlEditor(): TextResourceEditor {
|
||||
return this._sqlEditor;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposeEditors();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
let queryInput: QueryInput = <QueryInput>this.input;
|
||||
queryInput.sql.close();
|
||||
queryInput.results.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes visible the QueryResultsEditor for the current QueryInput (if it is not
|
||||
* already visible).
|
||||
*/
|
||||
public _showQueryResultsEditor(): void {
|
||||
if (this._isResultsEditorVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeControl = this._editorService.activeControl;
|
||||
activeControl.group.pinEditor(activeControl.input);
|
||||
|
||||
let input = <QueryInput>this.input;
|
||||
this._createResultsEditorContainer();
|
||||
|
||||
this._createEditor(<QueryResultsInput>input.results, this._resultsEditorContainer, this.group)
|
||||
.then(result => {
|
||||
this._onResultsEditorCreated(<any>result, input.results, this.options);
|
||||
this.resultsEditorVisibility = true;
|
||||
this.hideQueryResultsView = false;
|
||||
this._doLayout(true);
|
||||
});
|
||||
}
|
||||
|
||||
private hideQueryResultsView = false;
|
||||
|
||||
/**
|
||||
* Toggle the visibility of the view state of results
|
||||
*/
|
||||
public toggleResultsEditorVisibility(): void {
|
||||
let input = <QueryInput>this.input;
|
||||
let hideResults = this.hideQueryResultsView;
|
||||
this.hideQueryResultsView = !this.hideQueryResultsView;
|
||||
if (!input.results) {
|
||||
return;
|
||||
}
|
||||
this.resultsEditorVisibility = hideResults;
|
||||
this._doLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying SQL editor's text selection in a 0-indexed format. Returns undefined if there
|
||||
* is no selected text.
|
||||
*/
|
||||
public getSelection(checkIfRange: boolean = true): ISelectionData {
|
||||
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
||||
let vscodeSelection = this._sqlEditor.getControl().getSelection();
|
||||
|
||||
// If the selection is a range of characters rather than just a cursor position, return the range
|
||||
let isRange: boolean =
|
||||
!(vscodeSelection.getStartPosition().lineNumber === vscodeSelection.getEndPosition().lineNumber &&
|
||||
vscodeSelection.getStartPosition().column === vscodeSelection.getEndPosition().column);
|
||||
if (!checkIfRange || isRange) {
|
||||
let sqlToolsServiceSelection: ISelectionData = {
|
||||
startLine: vscodeSelection.getStartPosition().lineNumber - 1,
|
||||
startColumn: vscodeSelection.getStartPosition().column - 1,
|
||||
endLine: vscodeSelection.getEndPosition().lineNumber - 1,
|
||||
endColumn: vscodeSelection.getEndPosition().column - 1,
|
||||
};
|
||||
return sqlToolsServiceSelection;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise return undefined because there is no selected text
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public isSelectionEmpty(): boolean {
|
||||
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
||||
let control = this._sqlEditor.getControl();
|
||||
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
||||
|
||||
if (codeEditor) {
|
||||
let value = codeEditor.getValue();
|
||||
if (value !== undefined && value.length > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public getAllText(): string {
|
||||
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
||||
let control = this._sqlEditor.getControl();
|
||||
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
||||
if (codeEditor) {
|
||||
let value = codeEditor.getValue();
|
||||
if (value !== undefined && value.length > 0) {
|
||||
return value;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getAllSelection(): ISelectionData {
|
||||
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
||||
let control = this._sqlEditor.getControl();
|
||||
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
||||
if (codeEditor) {
|
||||
let model = codeEditor.getModel();
|
||||
let totalLines = model.getLineCount();
|
||||
let endColumn = model.getLineMaxColumn(totalLines);
|
||||
let selection: ISelectionData = {
|
||||
startLine: 0,
|
||||
startColumn: 0,
|
||||
endLine: totalLines - 1,
|
||||
endColumn: endColumn - 1,
|
||||
};
|
||||
return selection;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getSelectionText(): string {
|
||||
if (this._sqlEditor && this._sqlEditor.getControl()) {
|
||||
let control = this._sqlEditor.getControl();
|
||||
let codeEditor: ICodeEditor = <ICodeEditor>control;
|
||||
let vscodeSelection = control.getSelection();
|
||||
|
||||
if (codeEditor && vscodeSelection) {
|
||||
let model = codeEditor.getModel();
|
||||
let value = model.getValueInRange(vscodeSelection);
|
||||
if (value !== undefined && value.length > 0) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the run method of this editor's RunQueryAction
|
||||
*/
|
||||
public runQuery(): void {
|
||||
this._runQueryAction.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the runCurrent method of this editor's RunQueryAction
|
||||
*/
|
||||
public runCurrentQuery(): void {
|
||||
this._runQueryAction.runCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the runCurrentQueryWithActualPlan method of this editor's ActualQueryPlanAction
|
||||
*/
|
||||
public runCurrentQueryWithActualPlan(): void {
|
||||
this._actualQueryPlanAction.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the run method of this editor's CancelQueryAction
|
||||
*/
|
||||
public cancelQuery(): void {
|
||||
this._cancelQueryAction.run();
|
||||
}
|
||||
|
||||
public rebuildIntelliSenseCache(): void {
|
||||
this._connectionManagementService.rebuildIntelliSenseCache(this.connectedUri);
|
||||
}
|
||||
|
||||
public setOptions(options: EditorOptions): void {
|
||||
const textOptions = <TextEditorOptions>options;
|
||||
if (textOptions && types.isFunction(textOptions.apply)) {
|
||||
textOptions.apply(this.getControl() as editorCommon.IEditor, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS ////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Creates the query execution taskbar that appears at the top of the QueryEditor
|
||||
*/
|
||||
private _createTaskbar(parentElement: HTMLElement): void {
|
||||
// Create QueryTaskbar
|
||||
this._taskbarContainer = DOM.append(parentElement, DOM.$('div'));
|
||||
this._taskbar = new Taskbar(this._taskbarContainer, {
|
||||
actionItemProvider: (action: Action) => this._getActionItemForAction(action),
|
||||
});
|
||||
|
||||
// Create Actions for the toolbar
|
||||
this._runQueryAction = this._instantiationService.createInstance(RunQueryAction, this);
|
||||
this._cancelQueryAction = this._instantiationService.createInstance(CancelQueryAction, this);
|
||||
this._toggleConnectDatabaseAction = this._instantiationService.createInstance(ToggleConnectDatabaseAction, this, false);
|
||||
this._changeConnectionAction = this._instantiationService.createInstance(ConnectDatabaseAction, this, true);
|
||||
this._listDatabasesAction = this._instantiationService.createInstance(ListDatabasesAction, this);
|
||||
this._estimatedQueryPlanAction = this._instantiationService.createInstance(EstimatedQueryPlanAction, this);
|
||||
this._actualQueryPlanAction = this._instantiationService.createInstance(ActualQueryPlanAction, this);
|
||||
|
||||
this.setTaskbarContent();
|
||||
|
||||
this._toDispose.push(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectedKeys.includes('workbench.enablePreviewFeatures')) {
|
||||
this.setTaskbarContent();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private setTaskbarContent(): void {
|
||||
// Create HTML Elements for the taskbar
|
||||
let separator = Taskbar.createTaskbarSeparator();
|
||||
|
||||
// Set the content in the order we desire
|
||||
let content: ITaskbarContent[] = [
|
||||
{ action: this._runQueryAction },
|
||||
{ action: this._cancelQueryAction },
|
||||
{ element: separator },
|
||||
{ action: this._toggleConnectDatabaseAction },
|
||||
{ action: this._changeConnectionAction },
|
||||
{ action: this._listDatabasesAction },
|
||||
{ element: separator },
|
||||
{ action: this._estimatedQueryPlanAction }
|
||||
];
|
||||
|
||||
// Remove the estimated query plan action if preview features are not enabled
|
||||
let previewFeaturesEnabled = this._configurationService.getValue('workbench')['enablePreviewFeatures'];
|
||||
if (!previewFeaturesEnabled) {
|
||||
content = content.slice(0, -2);
|
||||
}
|
||||
|
||||
this._taskbar.setContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IActionItem for the List Databases dropdown if provided the associated Action.
|
||||
* Otherwise returns null.
|
||||
*/
|
||||
private _getActionItemForAction(action: Action): IActionItem {
|
||||
if (action.id === ListDatabasesAction.ID) {
|
||||
return this.listDatabasesActionItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public for testing purposes only
|
||||
*/
|
||||
public get listDatabasesActionItem(): ListDatabasesActionItem {
|
||||
if (!this._listDatabasesActionItem) {
|
||||
this._listDatabasesActionItem = this._instantiationService.createInstance(ListDatabasesActionItem, this);
|
||||
this._register(this._listDatabasesActionItem.attachStyler(this.themeService));
|
||||
}
|
||||
return this._listDatabasesActionItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setting input for this editor.
|
||||
*/
|
||||
private _updateInput(oldInput: QueryInput, newInput: QueryInput, 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.getQueryResultsInputResource();
|
||||
if (uri) {
|
||||
this._queryModelService.refreshResultsets(uri);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._sash) {
|
||||
if (this._isResultsEditorVisible()) {
|
||||
this._sash.show();
|
||||
} else {
|
||||
this._sash.hide();
|
||||
}
|
||||
}
|
||||
|
||||
this._updateTaskbar();
|
||||
return this._setNewInput(newInput, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles setting input and creating editors when this QueryEditor is either:
|
||||
* - Opened for the first time
|
||||
* - Opened with a new QueryInput
|
||||
* This will create only the SQL editor if the results editor does not yet exist for the
|
||||
* given QueryInput.
|
||||
*/
|
||||
private _setNewInput(newInput: QueryInput, 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(<QueryResultsInput>newInput.results, this._resultsEditorContainer, this.group),
|
||||
this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer, this.group)
|
||||
]);
|
||||
};
|
||||
onEditorsCreated = (result: IEditor[]) => {
|
||||
return Promise.all([
|
||||
this._onResultsEditorCreated(<QueryResultsEditor>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, this.group);
|
||||
};
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single editor based on the type of the given EditorInput.
|
||||
*/
|
||||
private _createEditor(editorInput: EditorInput, container: HTMLElement, group: IEditorGroup): 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(), group);
|
||||
return Promise.resolve(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets input for the results editor after it has been created.
|
||||
*/
|
||||
private _onResultsEditorCreated(resultsEditor: QueryResultsEditor, resultsInput: QueryResultsInput, options: EditorOptions): Promise<void> {
|
||||
this._resultsEditor = resultsEditor;
|
||||
return this._resultsEditor.setInput(resultsInput, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
this._sqlEditorContainer.style.position = 'absolute';
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the HTML for the QueryResultsEditor to the QueryEditor. 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 = <QueryInput>this.input;
|
||||
|
||||
if (!input.results.container) {
|
||||
let cssClass: string = '.master-editor-container';
|
||||
if (this._orientation === Orientation.HORIZONTAL) {
|
||||
cssClass = '.master-editor-container-horizontal';
|
||||
}
|
||||
|
||||
this._resultsEditorContainer = DOM.append(parentElement, DOM.$(cssClass));
|
||||
this._resultsEditorContainer.style.position = 'absolute';
|
||||
|
||||
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();
|
||||
|
||||
if (this._orientation === Orientation.HORIZONTAL) {
|
||||
this._sash = this._register(new HorizontalFlexibleSash(parentElement, this._minEditorSize));
|
||||
} else {
|
||||
this._sash = this._register(new VerticalFlexibleSash(parentElement, this._minEditorSize));
|
||||
this._sash.setEdge(this.getTaskBarHeight() + this._tabHeight);
|
||||
}
|
||||
this._setSashDimension();
|
||||
|
||||
this._register(this._sash.onPositionChange(position => this._doLayout()));
|
||||
}
|
||||
|
||||
this.sash.show();
|
||||
}
|
||||
|
||||
private _setSashDimension(): void {
|
||||
if (!this._dimension) {
|
||||
return;
|
||||
}
|
||||
if (this._orientation === Orientation.HORIZONTAL) {
|
||||
this._sash.setDimenesion(this._dimension);
|
||||
} else {
|
||||
this._sash.setDimenesion(new DOM.Dimension(this._dimension.width, this._dimension.height - this.getTaskBarHeight()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size of the 2 sub-editors. Uses agnostic dimensions due to the fact that
|
||||
* the IFlexibleSash could be horizontal or vertical. The same logic is used for horizontal
|
||||
* and vertical sashes.
|
||||
*/
|
||||
private _doLayout(skipResizeGridContent: boolean = false): void {
|
||||
if (!this._isResultsEditorVisible() && this._sqlEditor) {
|
||||
this._doLayoutSql();
|
||||
return;
|
||||
}
|
||||
if (!this._sqlEditor || !this._resultsEditor || !this._dimension || !this._sash) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._orientation === Orientation.HORIZONTAL) {
|
||||
this._doLayoutHorizontal();
|
||||
} else {
|
||||
this._doLayoutVertical();
|
||||
}
|
||||
|
||||
if (!skipResizeGridContent) {
|
||||
this._resizeGridContents();
|
||||
}
|
||||
}
|
||||
|
||||
private getTaskBarHeight(): number {
|
||||
let taskBarElement = this.taskbar.getContainer();
|
||||
return DOM.getContentHeight(taskBarElement);
|
||||
}
|
||||
|
||||
private _doLayoutHorizontal(): void {
|
||||
let splitPointTop: number = this._sash.getSplitPoint();
|
||||
let parent: ClientRect = this.getContainer().getBoundingClientRect();
|
||||
|
||||
let sqlEditorHeight = splitPointTop - (parent.top + this.getTaskBarHeight());
|
||||
|
||||
let titleBar = document.getElementById('workbench.parts.titlebar');
|
||||
if (titleBar) {
|
||||
sqlEditorHeight += DOM.getContentHeight(titleBar);
|
||||
}
|
||||
|
||||
let queryResultsEditorHeight = parent.bottom - splitPointTop;
|
||||
this._resultsEditorContainer.hidden = false;
|
||||
this._sqlEditorContainer.style.height = `${sqlEditorHeight}px`;
|
||||
this._sqlEditorContainer.style.width = `${this._dimension.width}px`;
|
||||
this._sqlEditorContainer.style.top = `${this._editorTopOffset}px`;
|
||||
|
||||
this._resultsEditorContainer.style.height = `${queryResultsEditorHeight}px`;
|
||||
this._resultsEditorContainer.style.width = `${this._dimension.width}px`;
|
||||
this._resultsEditorContainer.style.top = `${splitPointTop}px`;
|
||||
|
||||
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, sqlEditorHeight));
|
||||
this._resultsEditor.layout(new DOM.Dimension(this._dimension.width, queryResultsEditorHeight));
|
||||
}
|
||||
|
||||
private _doLayoutVertical(): void {
|
||||
let splitPointLeft: number = this._sash.getSplitPoint();
|
||||
let parent: ClientRect = this.getContainer().getBoundingClientRect();
|
||||
|
||||
let sqlEditorWidth = splitPointLeft;
|
||||
let queryResultsEditorWidth = parent.width - splitPointLeft;
|
||||
|
||||
let taskbarHeight = this.getTaskBarHeight();
|
||||
this._sqlEditorContainer.style.width = `${sqlEditorWidth}px`;
|
||||
this._sqlEditorContainer.style.height = `${this._dimension.height - taskbarHeight}px`;
|
||||
this._sqlEditorContainer.style.left = `0px`;
|
||||
|
||||
this._resultsEditorContainer.hidden = false;
|
||||
this._resultsEditorContainer.style.width = `${queryResultsEditorWidth}px`;
|
||||
this._resultsEditorContainer.style.height = `${this._dimension.height - taskbarHeight}px`;
|
||||
this._resultsEditorContainer.style.left = `${splitPointLeft}px`;
|
||||
|
||||
this._sqlEditor.layout(new DOM.Dimension(sqlEditorWidth, this._dimension.height - taskbarHeight));
|
||||
this._resultsEditor.layout(new DOM.Dimension(queryResultsEditorWidth, this._dimension.height - taskbarHeight));
|
||||
}
|
||||
|
||||
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) {
|
||||
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, this._dimension.height - this.getTaskBarHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
private _resizeGridContents(): void {
|
||||
if (this._isResultsEditorVisible()) {
|
||||
let queryInput: QueryInput = <QueryInput>this.input;
|
||||
let uri: string = queryInput.getQueryResultsInputResource();
|
||||
if (uri) {
|
||||
this._queryModelService.resizeResultsets(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the QueryResultsInput has denoted that the results editor
|
||||
* should be visible.
|
||||
* Public for testing only.
|
||||
*/
|
||||
public _isResultsEditorVisible(): boolean {
|
||||
let input: QueryInput = <QueryInput>this.input;
|
||||
|
||||
if (!input) {
|
||||
return false;
|
||||
}
|
||||
return input.results.visible;
|
||||
}
|
||||
|
||||
set resultsEditorVisibility(isVisible: boolean) {
|
||||
let input: QueryInput = <QueryInput>this.input;
|
||||
input.results.visible = isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the buttons on the taskbar to reflect the state of the current input.
|
||||
*/
|
||||
private _updateTaskbar(): void {
|
||||
let queryInput: QueryInput = <QueryInput>this.input;
|
||||
|
||||
if (queryInput) {
|
||||
this._cancelQueryAction.enabled = queryInput.cancelQueryEnabled;
|
||||
this._changeConnectionAction.enabled = queryInput.changeConnectionEnabled;
|
||||
|
||||
// For the toggle database action, it should always be enabled since it's a toggle.
|
||||
// We use inverse of connect enabled state for now, should refactor queryInput in the future to
|
||||
// define connected as a boolean instead of using the enabled flag
|
||||
this._toggleConnectDatabaseAction.enabled = true;
|
||||
this._toggleConnectDatabaseAction.connected = !queryInput.connectEnabled;
|
||||
this._runQueryAction.enabled = queryInput.runQueryEnabled;
|
||||
if (queryInput.listDatabasesConnected) {
|
||||
this.listDatabasesActionItem.onConnected();
|
||||
} else {
|
||||
this.listDatabasesActionItem.onDisconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text selection for the SQL editor based on the given ISelectionData.
|
||||
*/
|
||||
private _setSelection(selection: ISelectionData): void {
|
||||
let rangeConversion: IRange = {
|
||||
startLineNumber: selection.startLine + 1,
|
||||
startColumn: selection.startColumn + 1,
|
||||
endLineNumber: selection.endLine + 1,
|
||||
endColumn: selection.endColumn + 1
|
||||
};
|
||||
let editor = this._sqlEditor.getControl();
|
||||
editor.revealRange(rangeConversion);
|
||||
editor.setSelection(rangeConversion);
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
private saveEditorViewState(): void {
|
||||
let queryInput = this.input as QueryInput;
|
||||
if (queryInput) {
|
||||
if (this._sqlEditor) {
|
||||
queryInput.savedViewState = this._sqlEditor.getControl().saveViewState();
|
||||
}
|
||||
if (queryInput.results) {
|
||||
queryInput.results.onSaveViewStateEmitter.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TESTING PROPERTIES ////////////////////////////////////////////////////////////
|
||||
|
||||
public get resultsEditor(): QueryResultsEditor {
|
||||
return this._resultsEditor;
|
||||
}
|
||||
|
||||
public get sqlEditor(): TextResourceEditor {
|
||||
return this._sqlEditor;
|
||||
}
|
||||
|
||||
public get taskbar(): Taskbar {
|
||||
return this._taskbar;
|
||||
}
|
||||
|
||||
public get sash(): IFlexibleSash {
|
||||
return this._sash;
|
||||
}
|
||||
|
||||
public get resultsEditorContainer(): HTMLElement {
|
||||
return this._resultsEditorContainer;
|
||||
}
|
||||
|
||||
public get sqlEditorContainer(): HTMLElement {
|
||||
return this._sqlEditorContainer;
|
||||
}
|
||||
|
||||
public get taskbarContainer(): HTMLElement {
|
||||
return this._taskbarContainer;
|
||||
}
|
||||
|
||||
public get runQueryAction(): RunQueryAction {
|
||||
return this._runQueryAction;
|
||||
}
|
||||
|
||||
public get cancelQueryAction(): CancelQueryAction {
|
||||
return this._cancelQueryAction;
|
||||
}
|
||||
|
||||
public get changeConnectionAction(): ConnectDatabaseAction {
|
||||
return this._changeConnectionAction;
|
||||
}
|
||||
|
||||
public registerQueryModelViewTab(title: string, componentId: string): void {
|
||||
this._resultsEditor.registerQueryModelViewTab(title, componentId);
|
||||
}
|
||||
}
|
||||
170
src/sql/workbench/parts/query/browser/queryResultsEditor.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
import { QueryResultsInput } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import { QueryResultsView } from 'sql/workbench/parts/query/browser/queryResultsView';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export const RESULTS_GRID_DEFAULTS = {
|
||||
cellPadding: [5, 8, 4],
|
||||
rowHeight: 24
|
||||
};
|
||||
|
||||
export const TextCompareEditorVisible = new RawContextKey<boolean>('textCompareEditorVisible', false);
|
||||
|
||||
export class BareResultsGridInfo extends BareFontInfo {
|
||||
|
||||
public static createFromRawSettings(opts: {
|
||||
fontFamily?: string;
|
||||
fontWeight?: string;
|
||||
fontSize?: number | string;
|
||||
lineHeight?: number | string;
|
||||
letterSpacing?: number | string;
|
||||
cellPadding?: number | number[];
|
||||
}, zoomLevel: number): BareResultsGridInfo {
|
||||
let cellPadding = !types.isUndefinedOrNull(opts.cellPadding) ? opts.cellPadding : RESULTS_GRID_DEFAULTS.cellPadding;
|
||||
|
||||
return new BareResultsGridInfo(BareFontInfo.createFromRawSettings(opts, zoomLevel), { cellPadding });
|
||||
}
|
||||
|
||||
readonly cellPadding: number | number[];
|
||||
|
||||
protected constructor(fontInfo: BareFontInfo, opts: {
|
||||
cellPadding: number | number[];
|
||||
}) {
|
||||
super({
|
||||
zoomLevel: fontInfo.zoomLevel,
|
||||
fontFamily: fontInfo.fontFamily,
|
||||
fontWeight: fontInfo.fontWeight,
|
||||
fontSize: fontInfo.fontSize,
|
||||
lineHeight: fontInfo.lineHeight,
|
||||
letterSpacing: fontInfo.letterSpacing
|
||||
});
|
||||
this.cellPadding = opts.cellPadding;
|
||||
}
|
||||
}
|
||||
|
||||
function getBareResultsGridInfoStyles(info: BareResultsGridInfo): string {
|
||||
let content = '';
|
||||
if (info.fontFamily) {
|
||||
content += `font-family: ${info.fontFamily};`;
|
||||
}
|
||||
if (info.fontWeight) {
|
||||
content += `font-weight: ${info.fontWeight};`;
|
||||
}
|
||||
if (info.fontSize) {
|
||||
content += `font-size: ${info.fontSize}px;`;
|
||||
}
|
||||
if (info.lineHeight) {
|
||||
content += `line-height: ${info.lineHeight}px;`;
|
||||
}
|
||||
if (info.letterSpacing) {
|
||||
content += `letter-spacing: ${info.letterSpacing}px;`;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor associated with viewing and editing the data of a query results grid.
|
||||
*/
|
||||
export class QueryResultsEditor extends BaseEditor {
|
||||
|
||||
public static ID: string = 'workbench.editor.queryResultsEditor';
|
||||
public static AngularSelectorString: string = 'slickgrid-container.slickgridContainer';
|
||||
protected _rawOptions: BareResultsGridInfo;
|
||||
|
||||
private resultsView: QueryResultsView;
|
||||
private styleSheet = DOM.createStyleSheet();
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(QueryResultsEditor.ID, telemetryService, themeService, storageService);
|
||||
this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('resultsGrid')) {
|
||||
this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
|
||||
this.applySettings();
|
||||
}
|
||||
}));
|
||||
this.applySettings();
|
||||
}
|
||||
|
||||
public get input(): QueryResultsInput {
|
||||
return this._input as QueryResultsInput;
|
||||
}
|
||||
|
||||
private applySettings() {
|
||||
let cssRuleText = '';
|
||||
if (types.isNumber(this._rawOptions.cellPadding)) {
|
||||
cssRuleText = this._rawOptions.cellPadding + 'px';
|
||||
} else {
|
||||
cssRuleText = this._rawOptions.cellPadding.join('px ') + 'px;';
|
||||
}
|
||||
let content = `.grid-panel .monaco-table .slick-cell { padding: ${cssRuleText} }`;
|
||||
content += `.grid-panel .monaco-table, .message-tree { ${getBareResultsGridInfoStyles(this._rawOptions)} }`;
|
||||
this.styleSheet.innerHTML = content;
|
||||
}
|
||||
|
||||
createEditor(parent: HTMLElement): void {
|
||||
this.styleSheet.remove();
|
||||
parent.appendChild(this.styleSheet);
|
||||
if (!this.resultsView) {
|
||||
this.resultsView = this._register(new QueryResultsView(parent, this._instantiationService, this._queryModelService));
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.styleSheet.remove();
|
||||
this.styleSheet = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
this.resultsView.layout(dimension);
|
||||
}
|
||||
|
||||
setInput(input: QueryResultsInput, options: EditorOptions): Promise<void> {
|
||||
super.setInput(input, options, CancellationToken.None);
|
||||
this.resultsView.input = input;
|
||||
return Promise.resolve<void>(null);
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this.resultsView.clearInput();
|
||||
super.clearInput();
|
||||
}
|
||||
|
||||
public chart(dataId: { batchId: number, resultId: number }) {
|
||||
this.resultsView.chartData(dataId);
|
||||
}
|
||||
|
||||
public showQueryPlan(xml: string) {
|
||||
this.resultsView.showPlan(xml);
|
||||
}
|
||||
|
||||
public registerQueryModelViewTab(title: string, componentId: string): void {
|
||||
this.resultsView.registerQueryModelViewTab(title, componentId);
|
||||
}
|
||||
}
|
||||
364
src/sql/workbench/parts/query/browser/queryResultsView.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QueryResultsInput, ResultsViewState } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import { TabbedPanel, IPanelTab, IPanelView } from 'sql/base/browser/ui/panel/panel';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { MessagePanel } from './messagePanel';
|
||||
import { GridPanel } from '../electron-browser/gridPanel';
|
||||
import { ChartTab } from '../../charts/browser/chartTab';
|
||||
import { QueryPlanTab } from 'sql/workbench/parts/queryPlan/electron-browser/queryPlan';
|
||||
import { TopOperationsTab } from 'sql/workbench/parts/queryPlan/browser/topOperations';
|
||||
import { QueryModelViewTab } from 'sql/workbench/parts/query/modelViewTab/queryModelViewTab';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { PanelViewlet } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
class ResultsView extends Disposable implements IPanelView {
|
||||
private panelViewlet: PanelViewlet;
|
||||
private gridPanel: GridPanel;
|
||||
private messagePanel: MessagePanel;
|
||||
private container = document.createElement('div');
|
||||
private currentDimension: DOM.Dimension;
|
||||
private needsGridResize = false;
|
||||
private _state: ResultsViewState;
|
||||
|
||||
constructor(private instantiationService: IInstantiationService) {
|
||||
super();
|
||||
this.panelViewlet = this._register(this.instantiationService.createInstance(PanelViewlet, 'resultsView', { showHeaderInTitleWhenSingleView: false }));
|
||||
this.gridPanel = this._register(this.instantiationService.createInstance(GridPanel, { title: nls.localize('gridPanel', 'Results'), id: 'gridPanel' }));
|
||||
this.messagePanel = this._register(this.instantiationService.createInstance(MessagePanel, { title: nls.localize('messagePanel', 'Messages'), minimumBodySize: 0, id: 'messagePanel' }));
|
||||
this.gridPanel.render();
|
||||
this.messagePanel.render();
|
||||
this.panelViewlet.create(this.container);
|
||||
this.gridPanel.setVisible(false);
|
||||
this.panelViewlet.addPanels([
|
||||
{ panel: this.messagePanel, size: this.messagePanel.minimumSize, index: 1 }
|
||||
]);
|
||||
Event.any(this.gridPanel.onDidChange, this.messagePanel.onDidChange)(e => {
|
||||
let size = this.gridPanel.maximumBodySize;
|
||||
if (size < 1 && this.gridPanel.isVisible()) {
|
||||
this.gridPanel.setVisible(false);
|
||||
this.panelViewlet.removePanels([this.gridPanel]);
|
||||
this.gridPanel.layout(0);
|
||||
} else if (size > 0 && !this.gridPanel.isVisible()) {
|
||||
this.gridPanel.setVisible(true);
|
||||
let panelSize: number;
|
||||
if (this.state && this.state.gridPanelSize) {
|
||||
panelSize = this.state.gridPanelSize;
|
||||
} else if (this.currentDimension) {
|
||||
panelSize = Math.round(this.currentDimension.height * 0.7);
|
||||
} else {
|
||||
panelSize = 200;
|
||||
this.needsGridResize = true;
|
||||
}
|
||||
this.panelViewlet.addPanels([{ panel: this.gridPanel, index: 0, size: panelSize }]);
|
||||
}
|
||||
});
|
||||
let resizeList = Event.any(this.gridPanel.onDidChange, this.messagePanel.onDidChange)(() => {
|
||||
let panelSize: number;
|
||||
if (this.state && this.state.gridPanelSize) {
|
||||
panelSize = this.state.gridPanelSize;
|
||||
} else if (this.currentDimension) {
|
||||
panelSize = Math.round(this.currentDimension.height * 0.7);
|
||||
} else {
|
||||
panelSize = 200;
|
||||
this.needsGridResize = true;
|
||||
}
|
||||
if (this.state.messagePanelSize) {
|
||||
this.panelViewlet.resizePanel(this.gridPanel, this.state.messagePanelSize);
|
||||
}
|
||||
this.panelViewlet.resizePanel(this.gridPanel, panelSize);
|
||||
});
|
||||
// once the user changes the sash we should stop trying to resize the grid
|
||||
Event.once(this.panelViewlet.onDidSashChange)(e => {
|
||||
this.needsGridResize = false;
|
||||
resizeList.dispose();
|
||||
});
|
||||
|
||||
this.panelViewlet.onDidSashChange(e => {
|
||||
if (this.state) {
|
||||
if (this.gridPanel.isExpanded()) {
|
||||
this.state.gridPanelSize = this.panelViewlet.getPanelSize(this.gridPanel);
|
||||
}
|
||||
if (this.messagePanel.isExpanded()) {
|
||||
this.state.messagePanelSize = this.panelViewlet.getPanelSize(this.messagePanel);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
container.appendChild(this.container);
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
this.panelViewlet.layout(dimension);
|
||||
// the grid won't be resize if the height has not changed so we need to do it manually
|
||||
if (this.currentDimension && dimension.height === this.currentDimension.height) {
|
||||
this.gridPanel.layout(dimension.height);
|
||||
}
|
||||
this.currentDimension = dimension;
|
||||
if (this.needsGridResize) {
|
||||
this.panelViewlet.resizePanel(this.gridPanel, this.state.gridPanelSize || Math.round(this.currentDimension.height * 0.7));
|
||||
// we have the right scroll position saved as part of gridPanel state, use this to re-position scrollbar
|
||||
this.gridPanel.resetScrollPosition();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.gridPanel.clear();
|
||||
this.messagePanel.clear();
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this.gridPanel.queryRunner = runner;
|
||||
this.messagePanel.queryRunner = runner;
|
||||
}
|
||||
|
||||
public hideResultHeader() {
|
||||
this.gridPanel.headerVisible = false;
|
||||
}
|
||||
|
||||
public set state(val: ResultsViewState) {
|
||||
this._state = val;
|
||||
this.gridPanel.state = val.gridPanelState;
|
||||
this.messagePanel.state = val.messagePanelState;
|
||||
}
|
||||
|
||||
public get state(): ResultsViewState {
|
||||
return this._state;
|
||||
}
|
||||
}
|
||||
|
||||
class ResultsTab implements IPanelTab {
|
||||
public readonly title = nls.localize('resultsTabTitle', 'Results');
|
||||
public readonly identifier = 'resultsTab';
|
||||
public readonly view: ResultsView;
|
||||
|
||||
constructor(instantiationService: IInstantiationService) {
|
||||
this.view = new ResultsView(instantiationService);
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
this.view.queryRunner = runner;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.view);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.view.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryResultsView extends Disposable {
|
||||
private _panelView: TabbedPanel;
|
||||
private _input: QueryResultsInput;
|
||||
private resultsTab: ResultsTab;
|
||||
private chartTab: ChartTab;
|
||||
private qpTab: QueryPlanTab;
|
||||
private topOperationsTab: TopOperationsTab;
|
||||
private dynamicModelViewTabs: QueryModelViewTab[] = [];
|
||||
|
||||
private runnerDisposables: IDisposable[];
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IQueryModelService private queryModelService: IQueryModelService
|
||||
) {
|
||||
super();
|
||||
this.resultsTab = this._register(new ResultsTab(instantiationService));
|
||||
this.chartTab = this._register(new ChartTab(instantiationService));
|
||||
this._panelView = this._register(new TabbedPanel(container, { showHeaderWhenSingleView: false }));
|
||||
this.qpTab = this._register(new QueryPlanTab());
|
||||
this.topOperationsTab = this._register(new TopOperationsTab(instantiationService));
|
||||
|
||||
this._panelView.pushTab(this.resultsTab);
|
||||
this._register(this._panelView.onTabChange(e => {
|
||||
if (this.input) {
|
||||
this.input.state.activeTab = e;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public style() {
|
||||
}
|
||||
|
||||
private setQueryRunner(runner: QueryRunner) {
|
||||
this.resultsTab.queryRunner = runner;
|
||||
this.chartTab.queryRunner = runner;
|
||||
this.runnerDisposables.push(runner.onQueryStart(e => {
|
||||
this.hideChart();
|
||||
this.hidePlan();
|
||||
this.hideDynamicViewModelTabs();
|
||||
this.input.state.visibleTabs = new Set();
|
||||
this.input.state.activeTab = this.resultsTab.identifier;
|
||||
}));
|
||||
if (this.input.state.visibleTabs.has(this.chartTab.identifier)) {
|
||||
if (!this._panelView.contains(this.chartTab)) {
|
||||
this._panelView.pushTab(this.chartTab);
|
||||
}
|
||||
}
|
||||
if (this.input.state.visibleTabs.has(this.qpTab.identifier)) {
|
||||
if (!this._panelView.contains(this.qpTab)) {
|
||||
this._panelView.pushTab(this.qpTab);
|
||||
}
|
||||
}
|
||||
if (this.input.state.visibleTabs.has(this.topOperationsTab.identifier)) {
|
||||
if (!this._panelView.contains(this.topOperationsTab)) {
|
||||
this._panelView.pushTab(this.topOperationsTab);
|
||||
}
|
||||
}
|
||||
|
||||
// restore query model view tabs
|
||||
this.input.state.visibleTabs.forEach(tabId => {
|
||||
if (tabId.startsWith('querymodelview;')) {
|
||||
// tab id format is 'tab type;title;model view id'
|
||||
let parts = tabId.split(';');
|
||||
if (parts.length === 3) {
|
||||
let tab = this._register(new QueryModelViewTab(parts[1], this.instantiationService));
|
||||
tab.view._componentId = parts[2];
|
||||
this.dynamicModelViewTabs.push(tab);
|
||||
if (!this._panelView.contains(tab)) {
|
||||
this._panelView.pushTab(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.runnerDisposables.push(runner.onQueryEnd(() => {
|
||||
if (runner.isQueryPlan) {
|
||||
runner.planXml.then(e => {
|
||||
this.showPlan(e);
|
||||
});
|
||||
}
|
||||
}));
|
||||
if (this.input.state.activeTab) {
|
||||
this._panelView.showTab(this.input.state.activeTab);
|
||||
}
|
||||
}
|
||||
|
||||
public set input(input: QueryResultsInput) {
|
||||
this._input = input;
|
||||
dispose(this.runnerDisposables);
|
||||
this.runnerDisposables = [];
|
||||
this.resultsTab.view.state = this.input.state;
|
||||
this.qpTab.view.state = this.input.state.queryPlanState;
|
||||
this.topOperationsTab.view.state = this.input.state.topOperationsState;
|
||||
this.chartTab.view.state = this.input.state.chartState;
|
||||
|
||||
let info = this.queryModelService._getQueryInfo(input.uri);
|
||||
if (info) {
|
||||
this.setQueryRunner(info.queryRunner);
|
||||
} else {
|
||||
let disposeable = this.queryModelService.onRunQueryStart(c => {
|
||||
if (c === input.uri) {
|
||||
let info = this.queryModelService._getQueryInfo(input.uri);
|
||||
this.setQueryRunner(info.queryRunner);
|
||||
disposeable.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this._input = undefined;
|
||||
this.resultsTab.clear();
|
||||
this.qpTab.clear();
|
||||
this.topOperationsTab.clear();
|
||||
this.chartTab.clear();
|
||||
}
|
||||
|
||||
public get input(): QueryResultsInput {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
public layout(dimension: DOM.Dimension) {
|
||||
this._panelView.layout(dimension);
|
||||
}
|
||||
|
||||
public chartData(dataId: { resultId: number, batchId: number }): void {
|
||||
this.input.state.visibleTabs.add(this.chartTab.identifier);
|
||||
if (!this._panelView.contains(this.chartTab)) {
|
||||
this._panelView.pushTab(this.chartTab);
|
||||
}
|
||||
|
||||
this._panelView.showTab(this.chartTab.identifier);
|
||||
this.chartTab.chart(dataId);
|
||||
}
|
||||
|
||||
public hideChart() {
|
||||
if (this._panelView.contains(this.chartTab)) {
|
||||
this._panelView.removeTab(this.chartTab.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public showPlan(xml: string) {
|
||||
this.input.state.visibleTabs.add(this.qpTab.identifier);
|
||||
if (!this._panelView.contains(this.qpTab)) {
|
||||
this._panelView.pushTab(this.qpTab);
|
||||
}
|
||||
this.input.state.visibleTabs.add(this.topOperationsTab.identifier);
|
||||
if (!this._panelView.contains(this.topOperationsTab)) {
|
||||
this._panelView.pushTab(this.topOperationsTab);
|
||||
}
|
||||
|
||||
this._panelView.showTab(this.qpTab.identifier);
|
||||
this.qpTab.view.showPlan(xml);
|
||||
this.topOperationsTab.view.showPlan(xml);
|
||||
}
|
||||
|
||||
public hidePlan() {
|
||||
if (this._panelView.contains(this.qpTab)) {
|
||||
this._panelView.removeTab(this.qpTab.identifier);
|
||||
}
|
||||
|
||||
if (this._panelView.contains(this.topOperationsTab)) {
|
||||
this._panelView.removeTab(this.topOperationsTab.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
public hideDynamicViewModelTabs() {
|
||||
this.dynamicModelViewTabs.forEach(tab => {
|
||||
if (this._panelView.contains(tab)) {
|
||||
this._panelView.removeTab(tab.identifier);
|
||||
}
|
||||
});
|
||||
|
||||
this.dynamicModelViewTabs = [];
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.runnerDisposables);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public registerQueryModelViewTab(title: string, componentId: string): void {
|
||||
let tab = this._register(new QueryModelViewTab(title, this.instantiationService));
|
||||
tab.view._componentId = componentId;
|
||||
this.dynamicModelViewTabs.push(tab);
|
||||
|
||||
this.input.state.visibleTabs.add('querymodelview;' + title + ';' + componentId);
|
||||
if (!this._panelView.contains(tab)) {
|
||||
this._panelView.pushTab(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/sql/workbench/parts/query/browser/queryStatus.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $, append, show, hide } from 'vs/base/browser/dom';
|
||||
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import * as LocalizedConstants from 'sql/workbench/parts/query/common/localizedConstants';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
|
||||
// Query execution status
|
||||
enum QueryExecutionStatus {
|
||||
Executing,
|
||||
Completed
|
||||
}
|
||||
|
||||
// Shows query status in the editor
|
||||
export class QueryStatusbarItem implements IStatusbarItem {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private _queryElement: HTMLElement;
|
||||
private _queryStatusEditors: { [editorUri: string]: QueryExecutionStatus };
|
||||
private _toDispose: IDisposable[];
|
||||
|
||||
constructor(
|
||||
@IQueryModelService private _queryModelService: IQueryModelService,
|
||||
@IEditorService private _editorService: EditorServiceImpl
|
||||
) {
|
||||
this._queryStatusEditors = {};
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): IDisposable {
|
||||
this._element = append(container, $('.query-statusbar-group'));
|
||||
this._queryElement = append(this._element, $('div.query-statusbar-item'));
|
||||
hide(this._queryElement);
|
||||
|
||||
this._toDispose = [];
|
||||
this._toDispose.push(
|
||||
this._queryModelService.onRunQueryStart((uri: string) => this._onRunQueryStart(uri)),
|
||||
this._queryModelService.onRunQueryComplete((uri: string) => this._onRunQueryComplete(uri)),
|
||||
this._editorService.onDidVisibleEditorsChange(() => this._onEditorsChanged()),
|
||||
this._editorService.onDidCloseEditor(event => this._onEditorClosed(event))
|
||||
);
|
||||
|
||||
return combinedDisposable(this._toDispose);
|
||||
}
|
||||
|
||||
private _onEditorClosed(event: IEditorCloseEvent): void {
|
||||
let uri = WorkbenchUtils.getEditorUri(event.editor);
|
||||
if (uri && uri in this._queryStatusEditors) {
|
||||
// If active editor is being closed, hide the query status.
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (uri === currentUri) {
|
||||
hide(this._queryElement);
|
||||
}
|
||||
}
|
||||
delete this._queryStatusEditors[uri];
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorsChanged(): void {
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let uri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
|
||||
// Show active editor's query status
|
||||
if (uri && uri in this._queryStatusEditors) {
|
||||
this._showStatus(uri);
|
||||
} else {
|
||||
hide(this._queryElement);
|
||||
}
|
||||
} else {
|
||||
hide(this._queryElement);
|
||||
}
|
||||
}
|
||||
|
||||
private _onRunQueryStart(uri: string): void {
|
||||
this._updateStatus(uri, QueryExecutionStatus.Executing);
|
||||
}
|
||||
|
||||
private _onRunQueryComplete(uri: string): void {
|
||||
this._updateStatus(uri, QueryExecutionStatus.Completed);
|
||||
}
|
||||
|
||||
// Update query status for the editor
|
||||
private _updateStatus(uri: string, newStatus: QueryExecutionStatus) {
|
||||
if (uri) {
|
||||
this._queryStatusEditors[uri] = newStatus;
|
||||
this._showStatus(uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide query status for active editor
|
||||
private _showStatus(uri: string): void {
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (uri === currentUri) {
|
||||
switch (this._queryStatusEditors[uri]) {
|
||||
case QueryExecutionStatus.Executing:
|
||||
this._queryElement.textContent = LocalizedConstants.msgStatusRunQueryInProgress;
|
||||
show(this._queryElement);
|
||||
break;
|
||||
default:
|
||||
hide(this._queryElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/sql/workbench/parts/query/browser/rowCountStatus.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { IDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { append, $, hide, show } from 'vs/base/browser/dom';
|
||||
import * as nls from 'vs/nls';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
|
||||
export class RowCountStatusBarItem implements IStatusbarItem {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private _flavorElement: HTMLElement;
|
||||
|
||||
private dispose: IDisposable;
|
||||
|
||||
constructor(
|
||||
@IEditorService private _editorService: EditorServiceImpl,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService
|
||||
) { }
|
||||
|
||||
render(container: HTMLElement): IDisposable {
|
||||
let disposables = [
|
||||
this._editorService.onDidVisibleEditorsChange(() => this._onEditorsChanged()),
|
||||
this._editorService.onDidCloseEditor(event => this._onEditorClosed(event))
|
||||
];
|
||||
|
||||
this._element = append(container, $('.query-statusbar-group'));
|
||||
this._flavorElement = append(this._element, $('.editor-status-selection'));
|
||||
this._flavorElement.title = nls.localize('rowStatus', "Row Count");
|
||||
hide(this._flavorElement);
|
||||
|
||||
this._showStatus();
|
||||
|
||||
return combinedDisposable(disposables);
|
||||
}
|
||||
|
||||
private _onEditorsChanged() {
|
||||
this._showStatus();
|
||||
}
|
||||
|
||||
private _onEditorClosed(event: IEditorCloseEvent) {
|
||||
hide(this._flavorElement);
|
||||
}
|
||||
|
||||
// Show/hide query status for active editor
|
||||
private _showStatus(): void {
|
||||
hide(this._flavorElement);
|
||||
dispose(this.dispose);
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (currentUri) {
|
||||
let queryRunner = this._queryModelService.getQueryRunner(currentUri);
|
||||
if (queryRunner) {
|
||||
if (queryRunner.hasCompleted) {
|
||||
this._displayValue(queryRunner);
|
||||
}
|
||||
this.dispose = queryRunner.onQueryEnd(e => {
|
||||
this._displayValue(queryRunner);
|
||||
});
|
||||
} else {
|
||||
this.dispose = this._queryModelService.onRunQueryComplete(e => {
|
||||
if (e === currentUri) {
|
||||
this._displayValue(this._queryModelService.getQueryRunner(currentUri));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _displayValue(runner: QueryRunner) {
|
||||
let rowCount = runner.batchSets.reduce((p, c) => {
|
||||
return p + c.resultSetSummaries.reduce((rp, rc) => {
|
||||
return rp + rc.rowCount;
|
||||
}, 0);
|
||||
}, 0);
|
||||
this._flavorElement.innerText = nls.localize('rowCount', "{0} rows", rowCount);
|
||||
show(this._flavorElement);
|
||||
}
|
||||
}
|
||||
112
src/sql/workbench/parts/query/browser/timeElapsedStatus.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { parseNumAsTimeString } from 'sql/platform/connection/common/utils';
|
||||
|
||||
import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
|
||||
import { IDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { append, $, hide, show } from 'vs/base/browser/dom';
|
||||
import * as nls from 'vs/nls';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { IntervalTimer } from 'vs/base/common/async';
|
||||
|
||||
export class TimeElapsedStatusBarItem implements IStatusbarItem {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private _flavorElement: HTMLElement;
|
||||
|
||||
private dispose: IDisposable[] = [];
|
||||
private intervalTimer = new IntervalTimer();
|
||||
|
||||
constructor(
|
||||
@IEditorService private _editorService: EditorServiceImpl,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService
|
||||
) { }
|
||||
|
||||
render(container: HTMLElement): IDisposable {
|
||||
let disposables = [
|
||||
this._editorService.onDidVisibleEditorsChange(() => this._onEditorsChanged()),
|
||||
this._editorService.onDidCloseEditor(event => this._onEditorClosed(event))
|
||||
];
|
||||
|
||||
this._element = append(container, $('.query-statusbar-group'));
|
||||
this._flavorElement = append(this._element, $('.editor-status-selection'));
|
||||
this._flavorElement.title = nls.localize('timeElapsed', "Time Elapsed");
|
||||
hide(this._flavorElement);
|
||||
|
||||
this._showStatus();
|
||||
|
||||
return combinedDisposable(disposables);
|
||||
}
|
||||
|
||||
private _onEditorsChanged() {
|
||||
this._showStatus();
|
||||
}
|
||||
|
||||
private _onEditorClosed(event: IEditorCloseEvent) {
|
||||
hide(this._flavorElement);
|
||||
}
|
||||
|
||||
// Show/hide query status for active editor
|
||||
private _showStatus(): void {
|
||||
this.intervalTimer.cancel();
|
||||
hide(this._flavorElement);
|
||||
dispose(this.dispose);
|
||||
this._flavorElement.innerText = '';
|
||||
this.dispose = [];
|
||||
let activeEditor = this._editorService.activeControl;
|
||||
if (activeEditor) {
|
||||
let currentUri = WorkbenchUtils.getEditorUri(activeEditor.input);
|
||||
if (currentUri) {
|
||||
let queryRunner = this._queryModelService.getQueryRunner(currentUri);
|
||||
if (queryRunner) {
|
||||
if (queryRunner.hasCompleted || queryRunner.isExecuting) {
|
||||
this._displayValue(queryRunner);
|
||||
}
|
||||
this.dispose.push(queryRunner.onQueryStart(e => {
|
||||
this._displayValue(queryRunner);
|
||||
}));
|
||||
this.dispose.push(queryRunner.onQueryEnd(e => {
|
||||
this._displayValue(queryRunner);
|
||||
}));
|
||||
} else {
|
||||
this.dispose.push(this._queryModelService.onRunQueryStart(e => {
|
||||
if (e === currentUri) {
|
||||
this._displayValue(this._queryModelService.getQueryRunner(currentUri));
|
||||
}
|
||||
}));
|
||||
this.dispose.push(this._queryModelService.onRunQueryComplete(e => {
|
||||
if (e === currentUri) {
|
||||
this._displayValue(this._queryModelService.getQueryRunner(currentUri));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _displayValue(runner: QueryRunner) {
|
||||
this.intervalTimer.cancel();
|
||||
if (runner.isExecuting) {
|
||||
this.intervalTimer.cancelAndSet(() => {
|
||||
let value = runner.queryStartTime ? Date.now() - runner.queryStartTime.getTime() : 0;
|
||||
this._flavorElement.innerText = parseNumAsTimeString(value, false);
|
||||
}, 1000);
|
||||
|
||||
let value = runner.queryStartTime ? Date.now() - runner.queryStartTime.getTime() : 0;
|
||||
this._flavorElement.innerText = parseNumAsTimeString(value, false);
|
||||
} else {
|
||||
let value = runner.queryStartTime && runner.queryEndTime
|
||||
? runner.queryEndTime.getTime() - runner.queryStartTime.getTime() : 0;
|
||||
this._flavorElement.innerText = parseNumAsTimeString(value, false);
|
||||
}
|
||||
show(this._flavorElement);
|
||||
}
|
||||
}
|
||||
27
src/sql/workbench/parts/query/common/constants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const copyIncludeHeaders = 'copyIncludeHeaders';
|
||||
export const configSaveAsCsv = 'saveAsCsv';
|
||||
export const configSaveAsXml = 'saveAsXml';
|
||||
export const configCopyRemoveNewLine = 'copyRemoveNewLine';
|
||||
export const configShowBatchTime = 'showBatchTime';
|
||||
export const querySection = 'query';
|
||||
export const shortcutStart = 'shortcut';
|
||||
|
||||
export const tabColorModeOff = 'off';
|
||||
export const tabColorModeBorder = 'border';
|
||||
export const tabColorModeFill = 'fill';
|
||||
|
||||
export const defaultChartType = 'defaultChartType';
|
||||
export const chartTypeBar = 'bar';
|
||||
export const chartTypeDoughnut = 'doughnut';
|
||||
export const chartTypeHorizontalBar = 'horizontalBar';
|
||||
export const chartTypeLine = 'line';
|
||||
export const chartTypePie = 'pie';
|
||||
export const chartTypeScatter = 'scatter';
|
||||
export const chartTypeTimeSeries = 'timeSeries';
|
||||
export const allChartTypes = [chartTypeBar, chartTypeDoughnut, chartTypeHorizontalBar, chartTypeLine,
|
||||
chartTypePie, chartTypeScatter, chartTypeTimeSeries];
|
||||
38
src/sql/workbench/parts/query/common/localizedConstants.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
// localizable strings
|
||||
|
||||
export const runQueryBatchStartMessage = localize('runQueryBatchStartMessage', 'Started executing query at ');
|
||||
export const runQueryBatchStartLine = localize('runQueryBatchStartLine', 'Line {0}');
|
||||
|
||||
export const msgCancelQueryFailed = localize('msgCancelQueryFailed', 'Canceling the query failed: {0}');
|
||||
|
||||
export const msgSaveStarted = localize('msgSaveStarted', 'Started saving results to ');
|
||||
export const msgSaveFailed = localize('msgSaveFailed', 'Failed to save results. ');
|
||||
export const msgSaveSucceeded = localize('msgSaveSucceeded', 'Successfully saved results to ');
|
||||
|
||||
export const msgStatusRunQueryInProgress = localize('msgStatusRunQueryInProgress', 'Executing query...');
|
||||
|
||||
// /** Results Pane Labels */
|
||||
export const maximizeLabel = localize('maximizeLabel', 'Maximize');
|
||||
export const restoreLabel = localize('resultsPane.restoreLabel', 'Restore');
|
||||
export const saveCSVLabel = localize('saveCSVLabel', 'Save as CSV');
|
||||
export const saveJSONLabel = localize('saveJSONLabel', 'Save as JSON');
|
||||
export const saveExcelLabel = localize('saveExcelLabel', 'Save as Excel');
|
||||
export const saveXMLLabel = localize('saveXMLLabel', 'Save as XML');
|
||||
export const viewChartLabel = localize('viewChartLabel', 'View as Chart');
|
||||
|
||||
export const resultPaneLabel = localize('resultPaneLabel', 'Results');
|
||||
export const executeQueryLabel = localize('executeQueryLabel', 'Executing query ');
|
||||
|
||||
/** Messages Pane Labels */
|
||||
export const messagePaneLabel = localize('messagePaneLabel', 'Messages');
|
||||
export const elapsedTimeLabel = localize('elapsedTimeLabel', 'Total execution time: {0}');
|
||||
|
||||
/** Warning message for save icons */
|
||||
export const msgCannotSaveMultipleSelections = localize('msgCannotSaveMultipleSelections', 'Save results command cannot be used with multiple selections.');
|
||||
19
src/sql/workbench/parts/query/common/queryContext.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
/**
|
||||
* Context Keys to use with keybindings for the results grid and messages used in query and edit data views
|
||||
*/
|
||||
export const queryEditorVisibleId = 'queryEditorVisible';
|
||||
export const resultsVisibleId = 'resultsVisible';
|
||||
export const resultsGridFocussedId = 'resultsGridFocussed';
|
||||
export const resultsMessagesFocussedId = 'resultsMessagesFocussed';
|
||||
|
||||
export const QueryEditorVisibleContext = new RawContextKey<boolean>(queryEditorVisibleId, false);
|
||||
export const ResultsVisibleContext = new RawContextKey<boolean>(resultsVisibleId, false);
|
||||
export const ResultsGridFocussedContext = new RawContextKey<boolean>(resultsGridFocussedId, false);
|
||||
export const ResultsMessagesFocussedContext = new RawContextKey<boolean>(resultsMessagesFocussedId, false);
|
||||
319
src/sql/workbench/parts/query/common/queryInput.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { EditorInput, EditorModel, ConfirmResult, EncodingMode, IEncodingSupport } from 'vs/workbench/common/editor';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEditorViewState } from 'vs/editor/common/editorCommon';
|
||||
|
||||
import { IConnectionManagementService, IConnectableInput, INewConnectionParams, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { QueryResultsInput } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
|
||||
import { ISelectionData, ExecutionPlanOptions } from 'azdata';
|
||||
|
||||
const MAX_SIZE = 13;
|
||||
|
||||
function trimTitle(title: string): string {
|
||||
const length = title.length;
|
||||
const diff = length - MAX_SIZE;
|
||||
|
||||
if (Math.sign(diff) <= 0) {
|
||||
return title;
|
||||
} else {
|
||||
const start = (length / 2) - (diff / 2);
|
||||
return title.slice(0, start) + '...' + title.slice(start + diff, length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for the QueryEditor. This input is simply a wrapper around a QueryResultsInput for the QueryResultsEditor
|
||||
* and a UntitledEditorInput for the SQL File Editor.
|
||||
*/
|
||||
export class QueryInput extends EditorInput implements IEncodingSupport, IConnectableInput, IDisposable {
|
||||
|
||||
public static ID: string = 'workbench.editorinputs.queryInput';
|
||||
public static SCHEMA: string = 'sql';
|
||||
|
||||
private _runQueryEnabled: boolean;
|
||||
private _cancelQueryEnabled: boolean;
|
||||
private _connectEnabled: boolean;
|
||||
private _disconnectEnabled: boolean;
|
||||
private _changeConnectionEnabled: boolean;
|
||||
private _listDatabasesConnected: boolean;
|
||||
|
||||
private _updateTaskbar: Emitter<void>;
|
||||
private _showQueryResultsEditor: Emitter<void>;
|
||||
private _updateSelection: Emitter<ISelectionData>;
|
||||
|
||||
private _currentEventCallbacks: IDisposable[];
|
||||
|
||||
public savedViewState: IEditorViewState;
|
||||
|
||||
constructor(
|
||||
private _description: string,
|
||||
private _sql: UntitledEditorInput,
|
||||
private _results: QueryResultsInput,
|
||||
private _connectionProviderName: string,
|
||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||
@IQueryModelService private _queryModelService: IQueryModelService,
|
||||
@IQueryEditorService private _queryEditorService: IQueryEditorService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._updateTaskbar = new Emitter<void>();
|
||||
this._showQueryResultsEditor = new Emitter<void>();
|
||||
this._updateSelection = new Emitter<ISelectionData>();
|
||||
|
||||
this._toDispose = [];
|
||||
this._currentEventCallbacks = [];
|
||||
// re-emit sql editor events through this editor if it exists
|
||||
if (this._sql) {
|
||||
this._toDispose.push(this._sql.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
|
||||
}
|
||||
|
||||
// Attach to event callbacks
|
||||
if (this._queryModelService) {
|
||||
// Register callbacks for the Actions
|
||||
this._toDispose.push(
|
||||
this._queryModelService.onRunQueryStart(uri => {
|
||||
if (this.uri === uri) {
|
||||
this.onRunQuery();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._toDispose.push(
|
||||
this._queryModelService.onRunQueryComplete(uri => {
|
||||
if (this.uri === uri) {
|
||||
this.onQueryComplete();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this._connectionManagementService) {
|
||||
this._toDispose.push(this._connectionManagementService.onDisconnect(result => {
|
||||
if (result.connectionUri === this.uri) {
|
||||
this.onDisconnect();
|
||||
}
|
||||
}));
|
||||
if (this.uri) {
|
||||
if (this._connectionProviderName) {
|
||||
this._connectionManagementService.doChangeLanguageFlavor(this.uri, 'sql', this._connectionProviderName);
|
||||
} else {
|
||||
this._connectionManagementService.ensureDefaultLanguageFlavor(this.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._configurationService) {
|
||||
this._toDispose.push(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectedKeys.includes('sql.showConnectionInfoInTitle')) {
|
||||
this._onDidChangeLabel.fire();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.onDisconnect();
|
||||
this.onQueryComplete();
|
||||
}
|
||||
|
||||
// Getters for private properties
|
||||
public get uri(): string { return this.getResource().toString(); }
|
||||
public get sql(): UntitledEditorInput { return this._sql; }
|
||||
public get results(): QueryResultsInput { return this._results; }
|
||||
public get updateTaskbarEvent(): Event<void> { return this._updateTaskbar.event; }
|
||||
public get showQueryResultsEditorEvent(): Event<void> { return this._showQueryResultsEditor.event; }
|
||||
public get updateSelectionEvent(): Event<ISelectionData> { return this._updateSelection.event; }
|
||||
public get runQueryEnabled(): boolean { return this._runQueryEnabled; }
|
||||
public get cancelQueryEnabled(): boolean { return this._cancelQueryEnabled; }
|
||||
public get connectEnabled(): boolean { return this._connectEnabled; }
|
||||
public get disconnectEnabled(): boolean { return this._disconnectEnabled; }
|
||||
public get changeConnectionEnabled(): boolean { return this._changeConnectionEnabled; }
|
||||
public get listDatabasesConnected(): boolean { return this._listDatabasesConnected; }
|
||||
public getQueryResultsInputResource(): string { return this._results.uri; }
|
||||
public showQueryResultsEditor(): void { this._showQueryResultsEditor.fire(); }
|
||||
public updateSelection(selection: ISelectionData): void { this._updateSelection.fire(selection); }
|
||||
public getTypeId(): string { return QueryInput.ID; }
|
||||
// Description is shown beside the tab name in the combobox of open editors
|
||||
public getDescription(): string { return this._description; }
|
||||
public supportsSplitEditor(): boolean { return false; }
|
||||
public getModeId(): string { return QueryInput.SCHEMA; }
|
||||
public revert(): Promise<boolean> { return this._sql.revert(); }
|
||||
|
||||
public matches(otherInput: any): boolean {
|
||||
if (otherInput instanceof QueryInput) {
|
||||
return this._sql.matches(otherInput.sql);
|
||||
}
|
||||
|
||||
return this._sql.matches(otherInput);
|
||||
}
|
||||
|
||||
// Forwarding resource functions to the inline sql file editor
|
||||
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 save(): Promise<boolean> { return this._sql.save(); }
|
||||
public isDirty(): boolean { return this._sql.isDirty(); }
|
||||
public confirmSave(): Promise<ConfirmResult> { return this._sql.confirmSave(); }
|
||||
public getResource(): URI { return this._sql.getResource(); }
|
||||
public getEncoding(): string { return this._sql.getEncoding(); }
|
||||
public suggestFileName(): string { return this._sql.suggestFileName(); }
|
||||
|
||||
public getName(longForm?: boolean): string {
|
||||
if (this._configurationService.getValue('sql.showConnectionInfoInTitle')) {
|
||||
let profile = this._connectionManagementService.getConnectionProfile(this.uri);
|
||||
let title = '';
|
||||
if (this._description && this._description !== '') {
|
||||
title = this._description + ' ';
|
||||
}
|
||||
if (profile) {
|
||||
if (profile.userName) {
|
||||
title += `${profile.serverName}.${profile.databaseName} (${profile.userName})`;
|
||||
} else {
|
||||
title += `${profile.serverName}.${profile.databaseName} (${profile.authenticationType})`;
|
||||
}
|
||||
} else {
|
||||
title += localize('disconnected', 'disconnected');
|
||||
}
|
||||
return this._sql.getName() + (longForm ? (' - ' + title) : ` - ${trimTitle(title)}`);
|
||||
} else {
|
||||
return this._sql.getName();
|
||||
}
|
||||
}
|
||||
|
||||
// Called to get the tooltip of the tab
|
||||
public getTitle() {
|
||||
return this.getName(true);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// State update funtions
|
||||
public runQuery(selection: ISelectionData, executePlanOptions?: ExecutionPlanOptions): void {
|
||||
this._queryModelService.runQuery(this.uri, selection, this, executePlanOptions);
|
||||
this.showQueryResultsEditor();
|
||||
}
|
||||
|
||||
public runQueryStatement(selection: ISelectionData): void {
|
||||
this._queryModelService.runQueryStatement(this.uri, selection, this);
|
||||
this.showQueryResultsEditor();
|
||||
}
|
||||
|
||||
public runQueryString(text: string): void {
|
||||
this._queryModelService.runQueryString(this.uri, text, this);
|
||||
this.showQueryResultsEditor();
|
||||
}
|
||||
|
||||
public onConnectStart(): void {
|
||||
this._runQueryEnabled = false;
|
||||
this._cancelQueryEnabled = false;
|
||||
this._connectEnabled = false;
|
||||
this._disconnectEnabled = true;
|
||||
this._changeConnectionEnabled = false;
|
||||
this._listDatabasesConnected = false;
|
||||
this._updateTaskbar.fire();
|
||||
}
|
||||
|
||||
public onConnectReject(): void {
|
||||
this.onDisconnect();
|
||||
this._updateTaskbar.fire();
|
||||
}
|
||||
|
||||
public onConnectCanceled(): void {
|
||||
}
|
||||
|
||||
public onConnectSuccess(params?: INewConnectionParams): void {
|
||||
this._runQueryEnabled = true;
|
||||
this._connectEnabled = false;
|
||||
this._disconnectEnabled = true;
|
||||
this._changeConnectionEnabled = true;
|
||||
this._listDatabasesConnected = true;
|
||||
|
||||
let isRunningQuery = this._queryModelService.isRunningQuery(this.uri);
|
||||
if (!isRunningQuery && params && params.runQueryOnCompletion) {
|
||||
let selection: ISelectionData = params ? params.querySelection : undefined;
|
||||
if (params.runQueryOnCompletion === RunQueryOnConnectionMode.executeCurrentQuery) {
|
||||
this.runQueryStatement(selection);
|
||||
} else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.executeQuery) {
|
||||
this.runQuery(selection);
|
||||
} else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.estimatedQueryPlan) {
|
||||
this.runQuery(selection, { displayEstimatedQueryPlan: true });
|
||||
} else if (params.runQueryOnCompletion === RunQueryOnConnectionMode.actualQueryPlan) {
|
||||
this.runQuery(selection, { displayActualQueryPlan: true });
|
||||
}
|
||||
}
|
||||
this._updateTaskbar.fire();
|
||||
this._onDidChangeLabel.fire();
|
||||
}
|
||||
|
||||
public onDisconnect(): void {
|
||||
this._runQueryEnabled = true;
|
||||
this._cancelQueryEnabled = false;
|
||||
this._connectEnabled = true;
|
||||
this._disconnectEnabled = false;
|
||||
this._changeConnectionEnabled = false;
|
||||
this._listDatabasesConnected = false;
|
||||
this._updateTaskbar.fire();
|
||||
}
|
||||
|
||||
public onRunQuery(): void {
|
||||
this._runQueryEnabled = false;
|
||||
this._cancelQueryEnabled = true;
|
||||
this._updateTaskbar.fire();
|
||||
}
|
||||
|
||||
public onQueryComplete(): void {
|
||||
this._runQueryEnabled = true;
|
||||
this._cancelQueryEnabled = false;
|
||||
this._updateTaskbar.fire();
|
||||
}
|
||||
|
||||
// Clean up functions
|
||||
public dispose(): void {
|
||||
this._sql.dispose();
|
||||
this._results.dispose();
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
this._currentEventCallbacks = dispose(this._currentEventCallbacks);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._queryModelService.disposeQuery(this.uri);
|
||||
this._connectionManagementService.disconnectEditor(this, true);
|
||||
|
||||
this._sql.close();
|
||||
this._results.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe all events in _currentEventCallbacks and set the new callbacks
|
||||
* to be unsubscribed the next time this method is called.
|
||||
*
|
||||
* This method is used to ensure that all callbacks point to the current QueryEditor
|
||||
* in the case that this QueryInput is moved between different QueryEditors.
|
||||
*/
|
||||
public setEventCallbacks(callbacks: IDisposable[]): void {
|
||||
this._currentEventCallbacks = dispose(this._currentEventCallbacks);
|
||||
this._currentEventCallbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color that should be displayed
|
||||
*/
|
||||
public get tabColor(): string {
|
||||
return this._connectionManagementService.getTabColorForUri(this.uri);
|
||||
}
|
||||
}
|
||||
155
src/sql/workbench/parts/query/common/queryResultsInput.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { EditorInput } from 'vs/workbench/common/editor';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
import { GridPanelState } from 'sql/workbench/parts/query/electron-browser/gridPanel';
|
||||
import { MessagePanelState } from 'sql/workbench/parts/query/browser/messagePanel';
|
||||
import { QueryPlanState } from 'sql/workbench/parts/queryPlan/electron-browser/queryPlan';
|
||||
import { ChartState } from 'sql/workbench/parts/charts/browser/chartView';
|
||||
import { TopOperationsState } from 'sql/workbench/parts/queryPlan/browser/topOperations';
|
||||
|
||||
export class ResultsViewState {
|
||||
public gridPanelState: GridPanelState = new GridPanelState();
|
||||
public messagePanelState: MessagePanelState = new MessagePanelState(this.configurationService);
|
||||
public chartState: ChartState = new ChartState();
|
||||
public queryPlanState: QueryPlanState = new QueryPlanState();
|
||||
public topOperationsState = new TopOperationsState();
|
||||
public gridPanelSize: number;
|
||||
public messagePanelSize: number;
|
||||
public activeTab: string;
|
||||
public visibleTabs: Set<string> = new Set<string>();
|
||||
|
||||
constructor(@IConfigurationService private configurationService: IConfigurationService) {
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.gridPanelState.dispose();
|
||||
this.messagePanelState.dispose();
|
||||
this.chartState.dispose();
|
||||
this.queryPlanState.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for the QueryResultsEditor. This input helps with logic for the viewing and editing of
|
||||
* data in the results grid.
|
||||
*/
|
||||
export class QueryResultsInput 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>();
|
||||
|
||||
private _state = new ResultsViewState(this.configurationService);
|
||||
|
||||
public get state(): ResultsViewState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
constructor(private _uri: string,
|
||||
@IConfigurationService private configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._visible = false;
|
||||
this._hasBootstrapped = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.state.dispose();
|
||||
this._state = undefined;
|
||||
super.close();
|
||||
}
|
||||
|
||||
getTypeId(): string {
|
||||
return QueryResultsInput.ID;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return localize('extensionsInputName', 'Extension');
|
||||
}
|
||||
|
||||
matches(other: any): boolean {
|
||||
if (other instanceof QueryResultsInput) {
|
||||
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.query.queryResultsInput';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Extensions, IConfigurationRegistry, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as editorOptions from 'vs/editor/common/config/editorOptions';
|
||||
import EDITOR_FONT_DEFAULTS = editorOptions.EDITOR_FONT_DEFAULTS;
|
||||
|
||||
import { RESULTS_GRID_DEFAULTS } from 'sql/workbench/parts/query/browser/queryResultsEditor';
|
||||
|
||||
const configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
|
||||
|
||||
const resultsGridConfiguration: IConfigurationNode = {
|
||||
id: 'resultsGrid',
|
||||
type: 'object',
|
||||
title: nls.localize('resultsGridConfigurationTitle', "Results Grid"),
|
||||
overridable: true,
|
||||
properties: {
|
||||
'resultsGrid.fontFamily': {
|
||||
type: 'string',
|
||||
default: EDITOR_FONT_DEFAULTS.fontFamily,
|
||||
description: nls.localize('fontFamily', "Controls the font family.")
|
||||
},
|
||||
'resultsGrid.fontWeight': {
|
||||
type: 'string',
|
||||
enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
default: EDITOR_FONT_DEFAULTS.fontWeight,
|
||||
description: nls.localize('fontWeight', "Controls the font weight.")
|
||||
},
|
||||
'resultsGrid.fontSize': {
|
||||
type: 'number',
|
||||
default: EDITOR_FONT_DEFAULTS.fontSize,
|
||||
description: nls.localize('fontSize', "Controls the font size in pixels.")
|
||||
},
|
||||
'resultsGrid.letterSpacing': {
|
||||
type: 'number',
|
||||
default: EDITOR_FONT_DEFAULTS.letterSpacing,
|
||||
description: nls.localize('letterSpacing', "Controls the letter spacing in pixels.")
|
||||
},
|
||||
'resultsGrid.rowHeight': {
|
||||
type: 'number',
|
||||
default: RESULTS_GRID_DEFAULTS.rowHeight,
|
||||
description: nls.localize('rowHeight', "Controls the row height in pixels")
|
||||
},
|
||||
'resultsGrid.cellPadding': {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
],
|
||||
default: RESULTS_GRID_DEFAULTS.cellPadding,
|
||||
description: nls.localize('cellPadding', "Controls the cell padding in pixels")
|
||||
},
|
||||
'resultsGrid.autoSizeColumns': {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: nls.localize('autoSizeColumns', "Auto size the columns width on inital results. Could have performance problems with large number of columns or large cells")
|
||||
},
|
||||
'resultsGrid.maxColumnWidth': {
|
||||
type: 'number',
|
||||
default: 212,
|
||||
description: nls.localize('maxColumnWidth', "The maximum width in pixels for auto-sized columns")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
configurationRegistry.registerConfiguration(resultsGridConfiguration);
|
||||
814
src/sql/workbench/parts/query/electron-browser/gridPanel.ts
Normal file
@@ -0,0 +1,814 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as pretty from 'pretty-data';
|
||||
|
||||
import { attachTableStyler } from 'sql/platform/theme/common/styler';
|
||||
import QueryRunner from 'sql/platform/query/common/queryRunner';
|
||||
import { VirtualizedCollection, AsyncDataProvider } from 'sql/base/browser/ui/table/asyncDataView';
|
||||
import { Table } from 'sql/base/browser/ui/table/table';
|
||||
import { ScrollableSplitView, IView } from 'sql/base/browser/ui/scrollableSplitview/scrollableSplitview';
|
||||
import { MouseWheelSupport } from 'sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin';
|
||||
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
||||
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
|
||||
import { IGridActionContext, SaveResultAction, CopyResultAction, SelectAllGridAction, MaximizeTableAction, RestoreTableAction, ChartDataAction } from 'sql/workbench/parts/query/browser/actions';
|
||||
import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelectionModel.plugin';
|
||||
import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin';
|
||||
import { escape } from 'sql/base/common/strings';
|
||||
import { hyperLinkFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters';
|
||||
import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugin';
|
||||
import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin';
|
||||
import { ITableStyles, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces';
|
||||
import { warn } from 'sql/base/common/log';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import { Orientation } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { Separator, ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { isInDOM } from 'vs/base/browser/dom';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
|
||||
const ROW_HEIGHT = 29;
|
||||
const HEADER_HEIGHT = 26;
|
||||
const MIN_GRID_HEIGHT_ROWS = 8;
|
||||
const ESTIMATED_SCROLL_BAR_HEIGHT = 10;
|
||||
const BOTTOM_PADDING = 15;
|
||||
const ACTIONBAR_WIDTH = 36;
|
||||
|
||||
// minimum height needed to show the full actionbar
|
||||
const ACTIONBAR_HEIGHT = 120;
|
||||
|
||||
// this handles min size if rows is greater than the min grid visible rows
|
||||
const MIN_GRID_HEIGHT = (MIN_GRID_HEIGHT_ROWS * ROW_HEIGHT) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
|
||||
|
||||
export class GridPanelState {
|
||||
public tableStates: GridTableState[] = [];
|
||||
public scrollPosition: number;
|
||||
public collapsed = false;
|
||||
|
||||
dispose() {
|
||||
dispose(this.tableStates);
|
||||
}
|
||||
}
|
||||
|
||||
export class GridTableState extends Disposable {
|
||||
|
||||
private _maximized: boolean;
|
||||
|
||||
private _onMaximizedChange = this._register(new Emitter<boolean>());
|
||||
public onMaximizedChange: Event<boolean> = this._onMaximizedChange.event;
|
||||
|
||||
private _onCanBeMaximizedChange = this._register(new Emitter<boolean>());
|
||||
public onCanBeMaximizedChange: Event<boolean> = this._onCanBeMaximizedChange.event;
|
||||
|
||||
private _canBeMaximized: boolean;
|
||||
|
||||
/* The top row of the current scroll */
|
||||
public scrollPositionY = 0;
|
||||
public scrollPositionX = 0;
|
||||
public columnSizes: number[] = undefined;
|
||||
public selection: Slick.Range[];
|
||||
public activeCell: Slick.Cell;
|
||||
|
||||
constructor(public readonly resultId: number, public readonly batchId: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get canBeMaximized(): boolean {
|
||||
return this._canBeMaximized;
|
||||
}
|
||||
|
||||
public set canBeMaximized(val: boolean) {
|
||||
if (val === this._canBeMaximized) {
|
||||
return;
|
||||
}
|
||||
this._canBeMaximized = val;
|
||||
this._onCanBeMaximizedChange.fire(val);
|
||||
}
|
||||
|
||||
public get maximized(): boolean {
|
||||
return this._maximized;
|
||||
}
|
||||
|
||||
public set maximized(val: boolean) {
|
||||
if (val === this._maximized) {
|
||||
return;
|
||||
}
|
||||
this._maximized = val;
|
||||
this._onMaximizedChange.fire(val);
|
||||
}
|
||||
}
|
||||
|
||||
export class GridPanel extends ViewletPanel {
|
||||
private container = document.createElement('div');
|
||||
private splitView: ScrollableSplitView;
|
||||
private tables: GridTable<any>[] = [];
|
||||
private tableDisposable: IDisposable[] = [];
|
||||
private queryRunnerDisposables: IDisposable[] = [];
|
||||
private currentHeight: number;
|
||||
|
||||
private runner: QueryRunner;
|
||||
|
||||
private maximizedGrid: GridTable<any>;
|
||||
private _state: GridPanelState;
|
||||
|
||||
constructor(
|
||||
options: IViewletPanelOptions,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService);
|
||||
this.splitView = new ScrollableSplitView(this.container, { enableResizing: false, verticalScrollbarVisibility: ScrollbarVisibility.Visible });
|
||||
this.splitView.onScroll(e => {
|
||||
if (this.state && this.splitView.length !== 0) {
|
||||
this.state.scrollPosition = e;
|
||||
}
|
||||
});
|
||||
this.onDidChange(e => {
|
||||
if (this.state) {
|
||||
this.state.collapsed = !this.isExpanded();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
this.container.style.width = '100%';
|
||||
this.container.style.height = '100%';
|
||||
|
||||
container.appendChild(this.container);
|
||||
}
|
||||
|
||||
protected layoutBody(size: number): void {
|
||||
this.splitView.layout(size);
|
||||
// if the size hasn't change it won't layout our table so we have to do it manually
|
||||
if (size === this.currentHeight) {
|
||||
this.tables.map(e => e.layout());
|
||||
}
|
||||
this.currentHeight = size;
|
||||
}
|
||||
|
||||
public set queryRunner(runner: QueryRunner) {
|
||||
dispose(this.queryRunnerDisposables);
|
||||
this.reset();
|
||||
this.queryRunnerDisposables = [];
|
||||
this.runner = runner;
|
||||
this.queryRunnerDisposables.push(this.runner.onResultSet(this.onResultSet, this));
|
||||
this.queryRunnerDisposables.push(this.runner.onResultSetUpdate(this.updateResultSet, this));
|
||||
this.queryRunnerDisposables.push(this.runner.onQueryStart(() => {
|
||||
if (this.state) {
|
||||
this.state.tableStates = [];
|
||||
}
|
||||
this.reset();
|
||||
}));
|
||||
this.addResultSet(this.runner.batchSets.reduce<azdata.ResultSetSummary[]>((p, e) => {
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
p = p.concat(e.resultSetSummaries);
|
||||
} else {
|
||||
p = p.concat(e.resultSetSummaries.filter(c => c.complete));
|
||||
}
|
||||
return p;
|
||||
}, []));
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public resetScrollPosition(): void {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
|
||||
private onResultSet(resultSet: azdata.ResultSetSummary | azdata.ResultSetSummary[]) {
|
||||
let resultsToAdd: azdata.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToAdd = [resultSet];
|
||||
} else {
|
||||
resultsToAdd = resultSet.splice(0);
|
||||
}
|
||||
const sizeChanges = () => {
|
||||
this.tables.map(t => {
|
||||
t.state.canBeMaximized = this.tables.length > 1;
|
||||
});
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
this.addResultSet(resultsToAdd);
|
||||
sizeChanges();
|
||||
} else {
|
||||
resultsToAdd = resultsToAdd.filter(e => e.complete);
|
||||
if (resultsToAdd.length > 0) {
|
||||
this.addResultSet(resultsToAdd);
|
||||
}
|
||||
sizeChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private updateResultSet(resultSet: azdata.ResultSetSummary | azdata.ResultSetSummary[]) {
|
||||
let resultsToUpdate: azdata.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToUpdate = [resultSet];
|
||||
} else {
|
||||
resultsToUpdate = resultSet.splice(0);
|
||||
}
|
||||
|
||||
const sizeChanges = () => {
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
for (let set of resultsToUpdate) {
|
||||
let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id);
|
||||
if (table) {
|
||||
table.updateResult(set);
|
||||
} else {
|
||||
warn('Got result set update request for non-existant table');
|
||||
}
|
||||
}
|
||||
sizeChanges();
|
||||
} else {
|
||||
resultsToUpdate = resultsToUpdate.filter(e => e.complete);
|
||||
if (resultsToUpdate.length > 0) {
|
||||
this.addResultSet(resultsToUpdate);
|
||||
}
|
||||
sizeChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private addResultSet(resultSet: azdata.ResultSetSummary[]) {
|
||||
let tables: GridTable<any>[] = [];
|
||||
|
||||
for (let set of resultSet) {
|
||||
let tableState: GridTableState;
|
||||
if (this._state) {
|
||||
tableState = this.state.tableStates.find(e => e.batchId === set.batchId && e.resultId === set.id);
|
||||
}
|
||||
if (!tableState) {
|
||||
tableState = new GridTableState(set.id, set.batchId);
|
||||
if (this._state) {
|
||||
this._state.tableStates.push(tableState);
|
||||
}
|
||||
}
|
||||
let table = this.instantiationService.createInstance(GridTable, this.runner, set, tableState);
|
||||
this.tableDisposable.push(tableState.onMaximizedChange(e => {
|
||||
if (e) {
|
||||
this.maximizeTable(table.id);
|
||||
} else {
|
||||
this.minimizeTables();
|
||||
}
|
||||
}));
|
||||
this.tableDisposable.push(attachTableStyler(table, this.themeService));
|
||||
|
||||
tables.push(table);
|
||||
}
|
||||
|
||||
// possible to need a sort?
|
||||
|
||||
if (isUndefinedOrNull(this.maximizedGrid)) {
|
||||
this.splitView.addViews(tables, tables.map(i => i.minimumSize), this.splitView.length);
|
||||
}
|
||||
|
||||
this.tables = this.tables.concat(tables);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private reset() {
|
||||
for (let i = this.splitView.length - 1; i >= 0; i--) {
|
||||
this.splitView.removeView(i);
|
||||
}
|
||||
dispose(this.tables);
|
||||
dispose(this.tableDisposable);
|
||||
this.tableDisposable = [];
|
||||
this.tables = [];
|
||||
this.maximizedGrid = undefined;
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private maximizeTable(tableid: string): void {
|
||||
if (!this.tables.find(t => t.id === tableid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = this.tables.length - 1; i >= 0; i--) {
|
||||
if (this.tables[i].id === tableid) {
|
||||
this.tables[i].state.maximized = true;
|
||||
this.maximizedGrid = this.tables[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
this.splitView.removeView(i);
|
||||
}
|
||||
}
|
||||
|
||||
private minimizeTables(): void {
|
||||
if (this.maximizedGrid) {
|
||||
this.maximizedGrid.state.maximized = false;
|
||||
this.maximizedGrid = undefined;
|
||||
this.splitView.removeView(0);
|
||||
this.splitView.addViews(this.tables, this.tables.map(i => i.minimumSize));
|
||||
}
|
||||
}
|
||||
|
||||
public set state(val: GridPanelState) {
|
||||
this._state = val;
|
||||
this.tables.map(t => {
|
||||
let state = this.state.tableStates.find(s => s.batchId === t.resultSet.batchId && s.resultId === t.resultSet.id);
|
||||
if (!state) {
|
||||
this.state.tableStates.push(t.state);
|
||||
}
|
||||
if (state) {
|
||||
t.state = state;
|
||||
}
|
||||
});
|
||||
this.setExpanded(!this.state.collapsed);
|
||||
}
|
||||
|
||||
public get state(): GridPanelState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.queryRunnerDisposables);
|
||||
dispose(this.tableDisposable);
|
||||
dispose(this.tables);
|
||||
this.tableDisposable = undefined;
|
||||
this.tables = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class GridTable<T> extends Disposable implements IView {
|
||||
private table: Table<T>;
|
||||
private actionBar: ActionBar;
|
||||
private container = document.createElement('div');
|
||||
private selectionModel = new CellSelectionModel();
|
||||
private styles: ITableStyles;
|
||||
private currentHeight: number;
|
||||
private dataProvider: AsyncDataProvider<T>;
|
||||
|
||||
private columns: Slick.Column<T>[];
|
||||
|
||||
private rowNumberColumn: RowNumberColumn<T>;
|
||||
|
||||
private _onDidChange = new Emitter<number>();
|
||||
public readonly onDidChange: Event<number> = this._onDidChange.event;
|
||||
|
||||
public id = generateUuid();
|
||||
readonly element: HTMLElement = this.container;
|
||||
|
||||
private _state: GridTableState;
|
||||
|
||||
private scrolled = false;
|
||||
private visible = false;
|
||||
|
||||
private rowHeight: number;
|
||||
|
||||
public get resultSet(): azdata.ResultSetSummary {
|
||||
return this._resultSet;
|
||||
}
|
||||
|
||||
// this handles if the row count is small, like 4-5 rows
|
||||
private get maxSize(): number {
|
||||
return ((this.resultSet.rowCount) * this.rowHeight) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_HEIGHT;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private runner: QueryRunner,
|
||||
private _resultSet: azdata.ResultSetSummary,
|
||||
state: GridTableState,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
|
||||
@IConfigurationService private configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid');
|
||||
this.rowHeight = config && config.rowHeight ? config.rowHeight : ROW_HEIGHT;
|
||||
this.state = state;
|
||||
this.container.style.width = '100%';
|
||||
this.container.style.height = '100%';
|
||||
this.container.className = 'grid-panel';
|
||||
|
||||
this.columns = this.resultSet.columnInfo.map((c, i) => {
|
||||
let isLinked = c.isXml || c.isJson;
|
||||
|
||||
return <Slick.Column<T>>{
|
||||
id: i.toString(),
|
||||
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
|
||||
? 'XML Showplan'
|
||||
: escape(c.columnName),
|
||||
field: i.toString(),
|
||||
formatter: isLinked ? hyperLinkFormatter : textFormatter,
|
||||
width: this.state.columnSizes && this.state.columnSizes[i] ? this.state.columnSizes[i] : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public onAdd() {
|
||||
this.visible = true;
|
||||
let collection = new VirtualizedCollection(
|
||||
50,
|
||||
index => this.placeholdGenerator(index),
|
||||
this.resultSet.rowCount,
|
||||
(offset, count) => this.loadData(offset, count)
|
||||
);
|
||||
collection.setCollectionChangedCallback((startIndex, count) => {
|
||||
this.renderGridDataRowsRange(startIndex, count);
|
||||
});
|
||||
this.dataProvider.dataRows = collection;
|
||||
this.table.updateRowCount();
|
||||
this.setupState();
|
||||
}
|
||||
|
||||
public onRemove() {
|
||||
this.visible = false;
|
||||
let collection = new VirtualizedCollection(
|
||||
50,
|
||||
index => this.placeholdGenerator(index),
|
||||
0,
|
||||
() => Promise.resolve([])
|
||||
);
|
||||
this.dataProvider.dataRows = collection;
|
||||
this.table.updateRowCount();
|
||||
// when we are removed slickgrid acts badly so we need to account for that
|
||||
this.scrolled = false;
|
||||
}
|
||||
|
||||
private build(): void {
|
||||
let tableContainer = document.createElement('div');
|
||||
tableContainer.style.display = 'inline-block';
|
||||
tableContainer.style.width = `calc(100% - ${ACTIONBAR_WIDTH}px)`;
|
||||
|
||||
this.container.appendChild(tableContainer);
|
||||
|
||||
let collection = new VirtualizedCollection(
|
||||
50,
|
||||
index => this.placeholdGenerator(index),
|
||||
0,
|
||||
() => Promise.resolve([])
|
||||
);
|
||||
collection.setCollectionChangedCallback((startIndex, count) => {
|
||||
this.renderGridDataRowsRange(startIndex, count);
|
||||
});
|
||||
this.rowNumberColumn = new RowNumberColumn({ numberOfRows: this.resultSet.rowCount });
|
||||
let copyHandler = new CopyKeybind();
|
||||
copyHandler.onCopy(e => {
|
||||
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false).run(this.generateContext());
|
||||
});
|
||||
this.columns.unshift(this.rowNumberColumn.getColumnDefinition());
|
||||
let tableOptions: Slick.GridOptions<T> = {
|
||||
rowHeight: this.rowHeight,
|
||||
showRowNumber: true,
|
||||
forceFitColumns: false,
|
||||
defaultColumnWidth: 120
|
||||
};
|
||||
this.dataProvider = new AsyncDataProvider(collection);
|
||||
this.table = this._register(new Table(tableContainer, { dataProvider: this.dataProvider, columns: this.columns }, tableOptions));
|
||||
this.table.setSelectionModel(this.selectionModel);
|
||||
this.table.registerPlugin(new MouseWheelSupport());
|
||||
this.table.registerPlugin(new AutoColumnSize({ autoSizeOnRender: !this.state.columnSizes && this.configurationService.getValue('resultsGrid.autoSizeColumns'), maxWidth: this.configurationService.getValue<number>('resultsGrid.maxColumnWidth') }));
|
||||
this.table.registerPlugin(copyHandler);
|
||||
this.table.registerPlugin(this.rowNumberColumn);
|
||||
this.table.registerPlugin(new AdditionalKeyBindings());
|
||||
this._register(this.table.onContextMenu(this.contextMenu, this));
|
||||
this._register(this.table.onClick(this.onTableClick, this));
|
||||
|
||||
if (this.styles) {
|
||||
this.table.style(this.styles);
|
||||
}
|
||||
|
||||
let actions = this.getCurrentActions();
|
||||
|
||||
let actionBarContainer = document.createElement('div');
|
||||
actionBarContainer.style.width = ACTIONBAR_WIDTH + 'px';
|
||||
actionBarContainer.style.display = 'inline-block';
|
||||
actionBarContainer.style.height = '100%';
|
||||
actionBarContainer.style.verticalAlign = 'top';
|
||||
this.container.appendChild(actionBarContainer);
|
||||
this.actionBar = new ActionBar(actionBarContainer, {
|
||||
orientation: ActionsOrientation.VERTICAL, context: {
|
||||
runner: this.runner,
|
||||
batchId: this.resultSet.batchId,
|
||||
resultId: this.resultSet.id,
|
||||
table: this.table,
|
||||
tableState: this.state
|
||||
}
|
||||
});
|
||||
// update context before we run an action
|
||||
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
||||
this.actionBar.context = this.generateContext();
|
||||
});
|
||||
this.actionBar.push(actions, { icon: true, label: false });
|
||||
|
||||
this.selectionModel.onSelectedRangesChanged.subscribe(e => {
|
||||
if (this.state) {
|
||||
this.state.selection = this.selectionModel.getSelectedRanges();
|
||||
}
|
||||
});
|
||||
|
||||
this.table.grid.onScroll.subscribe((e, data) => {
|
||||
if (!this.visible) {
|
||||
// If the grid is not set up yet it can get scroll events resetting the top to 0px,
|
||||
// so ignore those events
|
||||
return;
|
||||
}
|
||||
if (!this.scrolled && (this.state.scrollPositionY || this.state.scrollPositionX) && isInDOM(this.container)) {
|
||||
this.scrolled = true;
|
||||
this.restoreScrollState();
|
||||
}
|
||||
if (this.state && isInDOM(this.container)) {
|
||||
this.state.scrollPositionY = data.scrollTop;
|
||||
this.state.scrollPositionX = data.scrollLeft;
|
||||
}
|
||||
});
|
||||
|
||||
// we need to remove the first column since this is the row number
|
||||
this.table.onColumnResize(() => {
|
||||
let columnSizes = this.table.grid.getColumns().slice(1).map(v => v.width);
|
||||
this.state.columnSizes = columnSizes;
|
||||
});
|
||||
|
||||
this.table.grid.onActiveCellChanged.subscribe(e => {
|
||||
if (this.state) {
|
||||
this.state.activeCell = this.table.grid.getActiveCell();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private restoreScrollState() {
|
||||
if (this.state.scrollPositionX || this.state.scrollPositionY) {
|
||||
this.table.grid.scrollTo(this.state.scrollPositionY);
|
||||
this.table.grid.getContainerNode().children[3].scrollLeft = this.state.scrollPositionX;
|
||||
}
|
||||
}
|
||||
|
||||
private setupState() {
|
||||
// change actionbar on maximize change
|
||||
this._register(this.state.onMaximizedChange(this.rebuildActionBar, this));
|
||||
|
||||
this._register(this.state.onCanBeMaximizedChange(this.rebuildActionBar, this));
|
||||
|
||||
this.restoreScrollState();
|
||||
|
||||
this.rebuildActionBar();
|
||||
|
||||
// Setting the active cell resets the selection so save it here
|
||||
let savedSelection = this.state.selection;
|
||||
|
||||
if (this.state.activeCell) {
|
||||
this.table.setActiveCell(this.state.activeCell.row, this.state.activeCell.cell);
|
||||
}
|
||||
|
||||
if (savedSelection) {
|
||||
this.selectionModel.setSelectedRanges(savedSelection);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): GridTableState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public set state(val: GridTableState) {
|
||||
this._state = val;
|
||||
}
|
||||
|
||||
private onTableClick(event: ITableMouseEvent) {
|
||||
// account for not having the number column
|
||||
let column = this.resultSet.columnInfo[event.cell.cell - 1];
|
||||
// handle if a showplan link was clicked
|
||||
if (column && (column.isXml || column.isJson)) {
|
||||
this.runner.getQueryRows(event.cell.row, 1, this.resultSet.batchId, this.resultSet.id).then(d => {
|
||||
let value = d.resultSubset.rows[0][event.cell.cell - 1];
|
||||
let content = value.displayValue;
|
||||
if (column.isXml) {
|
||||
try {
|
||||
content = pretty.pd.xml(content);
|
||||
} catch (e) {
|
||||
// If Xml fails to parse, fall back on original Xml content
|
||||
}
|
||||
} else {
|
||||
let jsonContent: string = undefined;
|
||||
try {
|
||||
jsonContent = JSON.parse(content);
|
||||
} catch (e) {
|
||||
// If Json fails to parse, fall back on original Json content
|
||||
}
|
||||
if (jsonContent) {
|
||||
// If Json content was valid and parsed, pretty print content to a string
|
||||
content = JSON.stringify(jsonContent, undefined, 4);
|
||||
}
|
||||
}
|
||||
|
||||
let input = this.untitledEditorService.createOrGet(undefined, column.isXml ? 'xml' : 'json', content);
|
||||
this.editorService.openEditor(input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public updateResult(resultSet: azdata.ResultSetSummary) {
|
||||
this._resultSet = resultSet;
|
||||
if (this.table && this.visible) {
|
||||
this.dataProvider.length = resultSet.rowCount;
|
||||
this.table.updateRowCount();
|
||||
}
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
private generateContext(cell?: Slick.Cell): IGridActionContext {
|
||||
const selection = this.selectionModel.getSelectedRanges();
|
||||
return <IGridActionContext>{
|
||||
cell,
|
||||
selection,
|
||||
runner: this.runner,
|
||||
batchId: this.resultSet.batchId,
|
||||
resultId: this.resultSet.id,
|
||||
table: this.table,
|
||||
tableState: this.state,
|
||||
selectionModel: this.selectionModel
|
||||
};
|
||||
}
|
||||
|
||||
private rebuildActionBar() {
|
||||
let actions = this.getCurrentActions();
|
||||
this.actionBar.clear();
|
||||
this.actionBar.push(actions, { icon: true, label: false });
|
||||
}
|
||||
|
||||
private 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(
|
||||
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 {
|
||||
if (!this.table) {
|
||||
this.build();
|
||||
}
|
||||
if (!size) {
|
||||
size = this.currentHeight;
|
||||
} else {
|
||||
this.currentHeight = size;
|
||||
}
|
||||
this.table.layout(size, Orientation.VERTICAL);
|
||||
}
|
||||
|
||||
public get minimumSize(): number {
|
||||
// clamp between ensuring we can show the actionbar, while also making sure we don't take too much space
|
||||
return Math.max(Math.min(this.maxSize, MIN_GRID_HEIGHT), ACTIONBAR_HEIGHT + BOTTOM_PADDING);
|
||||
}
|
||||
|
||||
public get maximumSize(): number {
|
||||
return Math.max(this.maxSize, ACTIONBAR_HEIGHT + BOTTOM_PADDING);
|
||||
}
|
||||
|
||||
private loadData(offset: number, count: number): Thenable<T[]> {
|
||||
return this.runner.getQueryRows(offset, count, this.resultSet.batchId, this.resultSet.id).then(response => {
|
||||
if (!response.resultSubset) {
|
||||
return [];
|
||||
}
|
||||
return response.resultSubset.rows.map(r => {
|
||||
let dataWithSchema = {};
|
||||
// skip the first column since its a number column
|
||||
for (let i = 1; i < this.columns.length; i++) {
|
||||
dataWithSchema[this.columns[i].field] = {
|
||||
displayValue: r[i - 1].displayValue,
|
||||
ariaLabel: escape(r[i - 1].displayValue),
|
||||
isNull: r[i - 1].isNull
|
||||
};
|
||||
}
|
||||
return dataWithSchema as T;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private contextMenu(e: ITableMouseEvent): void {
|
||||
const { cell } = e;
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => {
|
||||
let actions = [
|
||||
new SelectAllGridAction(),
|
||||
new Separator(),
|
||||
new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV),
|
||||
new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL),
|
||||
new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON),
|
||||
new SaveResultAction(SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML),
|
||||
new Separator(),
|
||||
new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false),
|
||||
new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true)
|
||||
];
|
||||
|
||||
if (this.state.canBeMaximized) {
|
||||
if (this.state.maximized) {
|
||||
actions.splice(1, 0, new RestoreTableAction());
|
||||
} else {
|
||||
actions.splice(1, 0, new MaximizeTableAction());
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
getActionsContext: () => {
|
||||
return this.generateContext(cell);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private placeholdGenerator(index: number): any {
|
||||
return {};
|
||||
}
|
||||
|
||||
private renderGridDataRowsRange(startIndex: number, count: number): void {
|
||||
// let editor = this.table.getCellEditor();
|
||||
// let oldValue = editor ? editor.getValue() : undefined;
|
||||
// let wasValueChanged = editor ? editor.isValueChanged() : false;
|
||||
this.invalidateRange(startIndex, startIndex + count);
|
||||
// let activeCell = this._grid.getActiveCell();
|
||||
// if (editor && activeCell.row >= startIndex && activeCell.row < startIndex + count) {
|
||||
// if (oldValue && wasValueChanged) {
|
||||
// editor.setValue(oldValue);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private invalidateRange(start: number, end: number): void {
|
||||
let refreshedRows = range(start, end);
|
||||
if (this.table) {
|
||||
this.table.invalidateRows(refreshedRows, true);
|
||||
}
|
||||
}
|
||||
|
||||
public style(styles: ITableStyles) {
|
||||
if (this.table) {
|
||||
this.table.style(styles);
|
||||
} else {
|
||||
this.styles = styles;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.container.remove();
|
||||
this.table.dispose();
|
||||
this.actionBar.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.dialogModal-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 500px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.modal.wide .dialogModal-body {
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.dialog-message-and-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialogModal-page-container {
|
||||
flex: 1 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialogModal-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.dialogModal-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer-button.dialogModal-hidden {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer-button .validating {
|
||||
background-size: 15px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.vs .footer-button .validating {
|
||||
background-image: url("loading.svg");
|
||||
}
|
||||
|
||||
.vs-dark .footer-button .validating,
|
||||
.hc-black .footer-button .validating {
|
||||
background-image: url("loading_inverse.svg");
|
||||
}
|
||||
|
||||
.dialogModal-wizardHeader {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
.dialogModal-wizardHeader h1 {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 3px;
|
||||
font-size: 1.5em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.dialogContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
31
src/sql/workbench/parts/query/modelViewTab/media/loading.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g style="fill:white;">
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,93 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.wizardNavigation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 80px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hc-black .wizardNavigation-container {
|
||||
border-right-color: #2b56f2;
|
||||
border-right-style: solid;
|
||||
border-right-width: 1px;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.wizardNavigation-pageNumber {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.wizardNavigation-pageNumber a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wizardNavigation-dot {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background-color: rgb(200, 200, 200);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
border-style: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hc-black .wizardNavigation-dot {
|
||||
flex-grow: 1;
|
||||
background-color: unset;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.wizardNavigation-connector {
|
||||
width: 3px;
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
background-color: rgb(200, 200, 200);
|
||||
}
|
||||
|
||||
.hc-black .wizardNavigation-connector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wizardNavigation-connector.active,
|
||||
.wizardNavigation-dot.active {
|
||||
background-color: rgb(9, 109, 201);
|
||||
}
|
||||
|
||||
.hc-black .wizardNavigation-dot.active {
|
||||
border-color: #2b56f2;
|
||||
background-color: unset;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.hc-black .wizardNavigation-dot.active:hover,
|
||||
.hc-black .wizardNavigation-dot.active.currentPage:hover {
|
||||
border-color: #F38518;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.wizardNavigation-dot.active.currentPage {
|
||||
border-style: double;
|
||||
}
|
||||
|
||||
.hc-black .wizardNavigation-dot.active.currentPage {
|
||||
border-style: solid;
|
||||
border-color: #F38518;
|
||||
}
|
||||
|
||||
.wizardNavigation-connector.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/dialogModal';
|
||||
|
||||
import { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { WizardNavigation } from 'sql/platform/dialog/wizardNavigation.component';
|
||||
import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
|
||||
import { ModelViewContent } from 'sql/workbench/electron-browser/modelComponents/modelViewContent.component';
|
||||
import { ModelComponentWrapper } from 'sql/workbench/electron-browser/modelComponents/modelComponentWrapper.component';
|
||||
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
|
||||
import { IBootstrapParams, ISelector, providerIterator } from 'sql/platform/bootstrap/node/bootstrapService';
|
||||
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component';
|
||||
import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component';
|
||||
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component';
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { QueryModelViewTabContainer } from 'sql/workbench/parts/query/modelViewTab/queryModelViewTabContainer.component';
|
||||
|
||||
export const QueryModelViewTabModule = (params, selector: string, instantiationService: IInstantiationService): any => {
|
||||
|
||||
/* Model-backed components */
|
||||
let extensionComponents = Registry.as<IComponentRegistry>(Extensions.ComponentContribution).getAllCtors();
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
Checkbox,
|
||||
SelectBox,
|
||||
EditableDropDown,
|
||||
InputBox,
|
||||
QueryModelViewTabContainer,
|
||||
WizardNavigation,
|
||||
ModelViewContent,
|
||||
ModelComponentWrapper,
|
||||
ComponentHostDirective,
|
||||
...extensionComponents
|
||||
],
|
||||
entryComponents: [QueryModelViewTabContainer, WizardNavigation, ...extensionComponents],
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
BrowserModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: '/' },
|
||||
CommonServiceInterface,
|
||||
{ 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) {
|
||||
let componentClass = QueryModelViewTabContainer;
|
||||
const factoryWrapper: any = this._resolver.resolveComponentFactory<QueryModelViewTabContainer>(componentClass);
|
||||
factoryWrapper.factory.selector = this.selector;
|
||||
appRef.bootstrap(factoryWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
return ModuleClass;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { dispose } from 'vs/base/common/lifecycle';
|
||||
import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { bootstrapAngular } from 'sql/platform/bootstrap/node/bootstrapService';
|
||||
import { QueryModelViewTabModule } from 'sql/workbench/parts/query/modelViewTab/queryModelViewTab.module';
|
||||
|
||||
export class QueryModelViewTab implements IPanelTab {
|
||||
public identifier = 'QueryModelViewTab_';
|
||||
public readonly view: QueryModelViewTabView;
|
||||
|
||||
constructor(public title: string, @IInstantiationService instantiationService: IInstantiationService) {
|
||||
this.identifier += title;
|
||||
this.view = instantiationService.createInstance(QueryModelViewTabView);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.view);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.view.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryModelViewTabView implements IPanelView {
|
||||
|
||||
public _componentId: string;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private _instantiationService: IInstantiationService) {
|
||||
}
|
||||
|
||||
public render(container: HTMLElement): void {
|
||||
this.bootstrapAngular(container);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
}
|
||||
|
||||
public clear() {
|
||||
}
|
||||
|
||||
public layout(dimension: Dimension): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the angular components and record for this input that we have done so
|
||||
*/
|
||||
private bootstrapAngular(container: HTMLElement): string {
|
||||
let uniqueSelector = bootstrapAngular(this._instantiationService,
|
||||
QueryModelViewTabModule,
|
||||
container,
|
||||
'querytab-modelview-container',
|
||||
{ modelViewId: this._componentId });
|
||||
return uniqueSelector;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/dialogModal';
|
||||
import { Component, ViewChild, Inject, forwardRef, ElementRef, AfterViewInit } from '@angular/core';
|
||||
import { ModelViewContent } from 'sql/workbench/electron-browser/modelComponents/modelViewContent.component';
|
||||
import { IBootstrapParams } from 'sql/platform/bootstrap/node/bootstrapService';
|
||||
import { DialogPane } from 'sql/platform/dialog/dialogPane';
|
||||
import { ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
export interface LayoutRequestParams {
|
||||
modelViewId?: string;
|
||||
alwaysRefresh?: boolean;
|
||||
}
|
||||
export interface DialogComponentParams extends IBootstrapParams {
|
||||
modelViewId: string;
|
||||
validityChangedCallback: (valid: boolean) => void;
|
||||
onLayoutRequested: Event<LayoutRequestParams>;
|
||||
dialogPane: DialogPane;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'querytab-modelview-container',
|
||||
providers: [],
|
||||
template: `
|
||||
<modelview-content [modelViewId]="modelViewId">
|
||||
</modelview-content>
|
||||
`
|
||||
})
|
||||
export class QueryModelViewTabContainer implements AfterViewInit {
|
||||
private _onResize = new Emitter<void>();
|
||||
public readonly onResize: Event<void> = this._onResize.event;
|
||||
|
||||
public modelViewId: string;
|
||||
@ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent;
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
|
||||
@Inject(IBootstrapParams) private _params: DialogComponentParams) {
|
||||
this.modelViewId = this._params.modelViewId;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._modelViewContent.onEvent(event => {
|
||||
if (event.isRootComponent && event.eventType === ComponentEventType.validityChanged) {
|
||||
this._params.validityChangedCallback(event.args);
|
||||
}
|
||||
});
|
||||
let element = <HTMLElement>this._el.nativeElement;
|
||||
element.style.height = '100%';
|
||||
element.style.width = '100%';
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
this._modelViewContent.layout();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/parts/query/editor/media/queryEditor';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
@@ -12,10 +11,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { QueryPlanInput } from 'sql/workbench/parts/queryPlan/common/queryPlanInput';
|
||||
import { QueryPlanModule } from 'sql/workbench/parts/queryPlan/electron-browser/queryPlan.module';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IMetadataService } from 'sql/platform/metadata/common/metadataService';
|
||||
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
|
||||
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
import { bootstrapAngular } from 'sql/platform/bootstrap/node/bootstrapService';
|
||||
import { IQueryPlanParams } from 'sql/platform/bootstrap/node/bootstrapParams';
|
||||
import { QUERYPLAN_SELECTOR } from 'sql/workbench/parts/queryPlan/electron-browser/queryPlan.component';
|
||||
@@ -30,11 +25,7 @@ export class QueryPlanEditor extends BaseEditor {
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IConnectionManagementService private _connectionService: IConnectionManagementService,
|
||||
@IMetadataService private _metadataService: IMetadataService,
|
||||
@IScriptingService private _scriptingService: IScriptingService,
|
||||
@IQueryEditorService private _queryEditorService: IQueryEditorService,
|
||||
@IStorageService private storageService: IStorageService
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(QueryPlanEditor.ID, telemetryService, themeService, storageService);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMess
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { escape } from 'sql/base/common/strings';
|
||||
import { elapsedTimeLabel } from 'sql/parts/query/common/localizedConstants';
|
||||
import { elapsedTimeLabel } from 'sql/workbench/parts/query/common/localizedConstants';
|
||||
import * as notebookUtils from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { QueryResultsInput } from 'sql/workbench/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
|
||||
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
|
||||
import { IConnectableInput, IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IQueryEditorService, IQueryEditorOptions } from 'sql/workbench/services/queryEditor/common/queryEditorService';
|
||||
|
||||