remove event emitter and use event and emitter instead (#5361)

This commit is contained in:
Anthony Dresser
2019-05-06 00:27:55 -07:00
committed by GitHub
parent 08ed9d285e
commit b9d985b663
13 changed files with 55 additions and 396 deletions

View File

@@ -1,301 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as errors from 'vs/base/common/errors';
import { IDisposable } from 'vs/base/common/lifecycle';
export class EmitterEvent {
public readonly type: string;
public readonly data: any;
constructor(eventType: string, data: any) {
this.type = eventType;
this.data = data;
}
}
export interface ListenerCallback {
(value: any): void;
}
export interface BulkListenerCallback {
(value: EmitterEvent[]): void;
}
export interface IBaseEventEmitter {
addBulkListener(listener: BulkListenerCallback): IDisposable;
}
export interface IEventEmitter extends IBaseEventEmitter, IDisposable {
addListener(eventType: string, listener: ListenerCallback): IDisposable;
addOneTimeListener(eventType: string, listener: ListenerCallback): IDisposable;
addEmitter(eventEmitter: IEventEmitter): IDisposable;
}
export interface IListenersMap {
[key: string]: ListenerCallback[];
}
export class EventEmitter implements IEventEmitter {
protected _listeners: IListenersMap;
protected _bulkListeners: ListenerCallback[];
private _collectedEvents: EmitterEvent[];
private _deferredCnt: number;
private _allowedEventTypes: { [eventType: string]: boolean; } | null;
constructor(allowedEventTypes?: string[]) {
this._listeners = {};
this._bulkListeners = [];
this._collectedEvents = [];
this._deferredCnt = 0;
if (allowedEventTypes) {
this._allowedEventTypes = {};
for (let i = 0; i < allowedEventTypes.length; i++) {
this._allowedEventTypes[allowedEventTypes[i]] = true;
}
} else {
this._allowedEventTypes = null;
}
}
public dispose(): void {
this._listeners = {};
this._bulkListeners = [];
this._collectedEvents = [];
this._deferredCnt = 0;
this._allowedEventTypes = null;
}
public addListener(eventType: string, listener: ListenerCallback): IDisposable {
if (eventType === '*') {
throw new Error('Use addBulkListener(listener) to register your listener!');
}
if (this._allowedEventTypes && !this._allowedEventTypes.hasOwnProperty(eventType)) {
throw new Error('This object will never emit this event type!');
}
if (this._listeners.hasOwnProperty(eventType)) {
this._listeners[eventType].push(listener);
} else {
this._listeners[eventType] = [listener];
}
let bound: this | null = this;
let _listener: ListenerCallback | null = listener;
return {
dispose: () => {
if (!bound) {
// Already called
return;
}
bound._removeListener(eventType, _listener!);
// Prevent leakers from holding on to the event emitter
bound = null;
_listener = null;
}
};
}
public addOneTimeListener(eventType: string, listener: ListenerCallback): IDisposable {
const disposable = this.addListener(eventType, value => {
disposable.dispose();
listener(value);
});
return disposable;
}
public addBulkListener(listener: BulkListenerCallback): IDisposable {
this._bulkListeners.push(listener);
return {
dispose: () => {
this._removeBulkListener(listener);
}
};
}
public addEmitter(eventEmitter: IBaseEventEmitter): IDisposable {
return eventEmitter.addBulkListener((events: EmitterEvent[]): void => {
if (this._deferredCnt === 0) {
this._emitEvents(events);
} else {
// Collect for later
this._collectedEvents.push.apply(this._collectedEvents, events);
}
});
}
private _removeListener(eventType: string, listener: ListenerCallback): void {
if (this._listeners.hasOwnProperty(eventType)) {
let listeners = this._listeners[eventType];
for (let i = 0, len = listeners.length; i < len; i++) {
if (listeners[i] === listener) {
listeners.splice(i, 1);
break;
}
}
}
}
private _removeBulkListener(listener: BulkListenerCallback): void {
for (let i = 0, len = this._bulkListeners.length; i < len; i++) {
if (this._bulkListeners[i] === listener) {
this._bulkListeners.splice(i, 1);
break;
}
}
}
protected _emitToSpecificTypeListeners(eventType: string, data: any): void {
if (this._listeners.hasOwnProperty(eventType)) {
const listeners = this._listeners[eventType].slice(0);
for (let i = 0, len = listeners.length; i < len; i++) {
safeInvoke1Arg(listeners[i], data);
}
}
}
protected _emitToBulkListeners(events: EmitterEvent[]): void {
const bulkListeners = this._bulkListeners.slice(0);
for (let i = 0, len = bulkListeners.length; i < len; i++) {
safeInvoke1Arg(bulkListeners[i], events);
}
}
protected _emitEvents(events: EmitterEvent[]): void {
if (this._bulkListeners.length > 0) {
this._emitToBulkListeners(events);
}
for (let i = 0, len = events.length; i < len; i++) {
const e = events[i];
this._emitToSpecificTypeListeners(e.type, e.data);
}
}
public emit(eventType: string, data: any = {}): void {
if (this._allowedEventTypes && !this._allowedEventTypes.hasOwnProperty(eventType)) {
throw new Error('Cannot emit this event type because it wasn\'t listed!');
}
// Early return if no listeners would get this
if (!this._listeners.hasOwnProperty(eventType) && this._bulkListeners.length === 0) {
return;
}
const emitterEvent = new EmitterEvent(eventType, data);
if (this._deferredCnt === 0) {
this._emitEvents([emitterEvent]);
} else {
// Collect for later
this._collectedEvents.push(emitterEvent);
}
}
public beginDeferredEmit(): void {
this._deferredCnt = this._deferredCnt + 1;
}
public endDeferredEmit(): void {
this._deferredCnt = this._deferredCnt - 1;
if (this._deferredCnt === 0) {
this._emitCollected();
}
}
public deferredEmit<T>(callback: () => T): T | undefined {
this.beginDeferredEmit();
let result: T | undefined = safeInvokeNoArg<T>(callback);
this.endDeferredEmit();
return result;
}
private _emitCollected(): void {
if (this._collectedEvents.length === 0) {
return;
}
// Flush collected events
const events = this._collectedEvents;
this._collectedEvents = [];
this._emitEvents(events);
}
}
class EmitQueueElement {
public target: Function;
public arg: any;
constructor(target: Function, arg: any) {
this.target = target;
this.arg = arg;
}
}
/**
* Same as EventEmitter, but guarantees events are delivered in order to each listener
*/
export class OrderGuaranteeEventEmitter extends EventEmitter {
private _emitQueue: EmitQueueElement[];
constructor() {
super();
this._emitQueue = [];
}
protected _emitToSpecificTypeListeners(eventType: string, data: any): void {
if (this._listeners.hasOwnProperty(eventType)) {
let listeners = this._listeners[eventType];
for (let i = 0, len = listeners.length; i < len; i++) {
this._emitQueue.push(new EmitQueueElement(listeners[i], data));
}
}
}
protected _emitToBulkListeners(events: EmitterEvent[]): void {
let bulkListeners = this._bulkListeners;
for (let i = 0, len = bulkListeners.length; i < len; i++) {
this._emitQueue.push(new EmitQueueElement(bulkListeners[i], events));
}
}
protected _emitEvents(events: EmitterEvent[]): void {
super._emitEvents(events);
while (this._emitQueue.length > 0) {
let queueElement = this._emitQueue.shift();
if (queueElement) {
safeInvoke1Arg(queueElement.target, queueElement.arg);
}
}
}
}
function safeInvokeNoArg<T>(func: Function): T | undefined {
try {
return func();
} catch (e) {
errors.onUnexpectedError(e);
}
return undefined;
}
function safeInvoke1Arg(func: Function, arg1: any): any {
try {
return func(arg1);
} catch (e) {
errors.onUnexpectedError(e);
}
}

View File

@@ -5,7 +5,7 @@
import * as GridContentEvents from 'sql/workbench/parts/grid/common/gridContentEvents';
import * as LocalizedConstants from 'sql/workbench/parts/query/common/localizedConstants';
import QueryRunner, { EventType as QREvents } from 'sql/platform/query/common/queryRunner';
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { DataService } from 'sql/workbench/parts/grid/services/dataService';
import { IQueryModelService, IQueryEvent } from 'sql/platform/query/common/queryModel';
import { QueryInput } from 'sql/workbench/parts/query/common/queryInput';
@@ -277,10 +277,10 @@ export class QueryModelService implements IQueryModelService {
private initQueryRunner(uri: string): QueryInfo {
let queryRunner = this._instantiationService.createInstance(QueryRunner, uri);
let info = new QueryInfo();
queryRunner.addListener(QREvents.RESULT_SET, e => {
queryRunner.onResultSet(e => {
this._fireQueryEvent(uri, 'resultSet', e);
});
queryRunner.addListener(QREvents.BATCH_START, b => {
queryRunner.onBatchStart(b => {
let link = undefined;
let messageText = LocalizedConstants.runQueryBatchStartMessage;
if (b.selection) {
@@ -304,10 +304,10 @@ export class QueryModelService implements IQueryModelService {
this._fireQueryEvent(uri, 'message', message);
info.selection.push(this._validateSelection(b.selection));
});
queryRunner.addListener(QREvents.MESSAGE, m => {
queryRunner.onMessage(m => {
this._fireQueryEvent(uri, 'message', m);
});
queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => {
queryRunner.onQueryEnd(totalMilliseconds => {
this._onRunQueryComplete.fire(uri);
// fire extensibility API event
@@ -320,7 +320,7 @@ export class QueryModelService implements IQueryModelService {
// fire UI event
this._fireQueryEvent(uri, 'complete', totalMilliseconds);
});
queryRunner.addListener(QREvents.START, () => {
queryRunner.onQueryStart(() => {
this._onRunQueryStart.fire(uri);
// fire extensibility API event
@@ -333,7 +333,7 @@ export class QueryModelService implements IQueryModelService {
this._fireQueryEvent(uri, 'start');
});
queryRunner.addListener(QREvents.QUERY_PLAN_AVAILABLE, (planInfo) => {
queryRunner.onQueryPlanAvailable(planInfo => {
// fire extensibility API event
let event: IQueryEvent = {
type: 'executionPlan',
@@ -416,13 +416,13 @@ export class QueryModelService implements IQueryModelService {
// and map it to the results uri
queryRunner = this._instantiationService.createInstance(QueryRunner, ownerUri);
const resultSetEventType = 'resultSet';
queryRunner.addListener(QREvents.RESULT_SET, resultSet => {
queryRunner.onResultSet(resultSet => {
this._fireQueryEvent(ownerUri, resultSetEventType, resultSet);
});
queryRunner.onResultSetUpdate(resultSetSummary => {
this._fireQueryEvent(ownerUri, resultSetEventType, resultSetSummary);
});
queryRunner.addListener(QREvents.BATCH_START, batch => {
queryRunner.onBatchStart(batch => {
let link = undefined;
let messageText = LocalizedConstants.runQueryBatchStartMessage;
if (batch.selection) {
@@ -445,10 +445,10 @@ export class QueryModelService implements IQueryModelService {
};
this._fireQueryEvent(ownerUri, 'message', message);
});
queryRunner.addListener(QREvents.MESSAGE, message => {
queryRunner.onMessage(message => {
this._fireQueryEvent(ownerUri, 'message', message);
});
queryRunner.addListener(QREvents.COMPLETE, totalMilliseconds => {
queryRunner.onQueryEnd(totalMilliseconds => {
this._onRunQueryComplete.fire(ownerUri);
// fire extensibility API event
let event: IQueryEvent = {
@@ -460,7 +460,7 @@ export class QueryModelService implements IQueryModelService {
// fire UI event
this._fireQueryEvent(ownerUri, 'complete', totalMilliseconds);
});
queryRunner.addListener(QREvents.START, () => {
queryRunner.onQueryStart(() => {
this._onRunQueryStart.fire(ownerUri);
// fire extensibility API event
let event: IQueryEvent = {
@@ -472,7 +472,7 @@ export class QueryModelService implements IQueryModelService {
// fire UI event
this._fireQueryEvent(ownerUri, 'start');
});
queryRunner.addListener(QREvents.EDIT_SESSION_READY, e => {
queryRunner.onEditSessionReady(e => {
this._onEditSessionReady.fire(e);
this._fireQueryEvent(e.ownerUri, 'editSessionReady');
});

View File

@@ -11,19 +11,18 @@ import { IQueryManagementService } from 'sql/platform/query/common/queryManageme
import * as Utils from 'sql/platform/connection/common/utils';
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
import { Deferred } from 'sql/base/common/promise';
import { IQueryPlanInfo } from 'sql/platform/query/common/queryModel';
import { ResultSerializer } from 'sql/platform/node/resultSerializer';
import Severity from 'vs/base/common/severity';
import * as nls from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import * as types from 'vs/base/common/types';
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Emitter, Event } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResultSerializer } from 'sql/platform/node/resultSerializer';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IQueryPlanInfo } from 'sql/platform/query/common/queryModel';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { URI } from 'vs/base/common/uri';
@@ -33,28 +32,6 @@ export interface IEditSessionReadyEvent {
message: string;
}
export const enum EventType {
START = 'start',
COMPLETE = 'complete',
MESSAGE = 'message',
BATCH_START = 'batchStart',
BATCH_COMPLETE = 'batchComplete',
RESULT_SET = 'resultSet',
EDIT_SESSION_READY = 'editSessionReady',
QUERY_PLAN_AVAILABLE = 'queryPlanAvailable'
}
export interface IEventType {
start: void;
complete: string;
message: azdata.IResultMessage;
batchStart: azdata.BatchSummary;
batchComplete: azdata.BatchSummary;
resultSet: azdata.ResultSetSummary;
editSessionReady: IEditSessionReadyEvent;
queryPlanAvailable: IQueryPlanInfo;
}
export interface IGridMessage extends azdata.IResultMessage {
selection: azdata.ISelectionData;
}
@@ -71,7 +48,6 @@ export default class QueryRunner extends Disposable {
private _hasCompleted: boolean = false;
private _batchSets: azdata.BatchSummary[] = [];
private _messages: azdata.IResultMessage[] = [];
private _eventEmitter = new EventEmitter();
private registered = false;
private _isQueryPlan: boolean;
@@ -80,7 +56,7 @@ export default class QueryRunner extends Disposable {
public get planXml(): Thenable<string> { return this._planXml.promise; }
private _onMessage = this._register(new Emitter<azdata.IResultMessage>());
public readonly onMessage = this._onMessage.event;
public get onMessage(): Event<azdata.IResultMessage> { return this._onMessage.event; } // this is the only way typemoq can moq this... needs investigation @todo anthonydresser 5/2/2019
private _onResultSet = this._register(new Emitter<azdata.ResultSetSummary>());
public readonly onResultSet = this._onResultSet.event;
@@ -92,7 +68,7 @@ export default class QueryRunner extends Disposable {
public readonly onQueryStart: Event<void> = this._onQueryStart.event;
private _onQueryEnd = this._register(new Emitter<string>());
public readonly onQueryEnd: Event<string> = this._onQueryEnd.event;
public get onQueryEnd(): Event<string> { return this._onQueryEnd.event; }
private _onBatchStart = this._register(new Emitter<azdata.BatchSummary>());
public readonly onBatchStart: Event<azdata.BatchSummary> = this._onBatchStart.event;
@@ -100,6 +76,12 @@ export default class QueryRunner extends Disposable {
private _onBatchEnd = this._register(new Emitter<azdata.BatchSummary>());
public readonly onBatchEnd: Event<azdata.BatchSummary> = this._onBatchEnd.event;
private _onEditSessionReady = this._register(new Emitter<IEditSessionReadyEvent>());
public readonly onEditSessionReady = this._onEditSessionReady.event;
private _onQueryPlanAvailable = this._register(new Emitter<IQueryPlanInfo>());
public readonly onQueryPlanAvailable = this._onQueryPlanAvailable.event;
private _queryStartTime: Date;
public get queryStartTime(): Date {
return this._queryStartTime;
@@ -146,10 +128,6 @@ export default class QueryRunner extends Disposable {
// PUBLIC METHODS ======================================================
public addListener<K extends keyof IEventType>(event: K, f: (e: IEventType[K]) => void): IDisposable {
return this._eventEmitter.addListener(event, f);
}
/**
* Cancels the running query, if there is one
*/
@@ -229,7 +207,6 @@ export default class QueryRunner extends Disposable {
// this isn't exact, but its the best we can do
this._queryStartTime = new Date();
// The query has started, so lets fire up the result pane
this._eventEmitter.emit(EventType.START);
if (!this.registered) {
this.registered = true;
this._queryManagementService.registerRunner(this, this.uri);
@@ -270,8 +247,6 @@ export default class QueryRunner extends Disposable {
});
let timeStamp = Utils.parseNumAsTimeString(this._totalElapsedMilliseconds);
this._eventEmitter.emit(EventType.COMPLETE, timeStamp);
// We're done with this query so shut down any waiting mechanisms
let message = {
@@ -311,7 +286,6 @@ export default class QueryRunner extends Disposable {
isError: false
};
this._messages.push(message);
this._eventEmitter.emit(EventType.BATCH_START, batch);
this._onMessage.fire(message);
this._onBatchStart.fire(batch);
}
@@ -331,7 +305,6 @@ export default class QueryRunner extends Disposable {
this.sendBatchTimeMessage(batch.id, Utils.parseNumAsTimeString(executionTime));
}
this._eventEmitter.emit(EventType.BATCH_COMPLETE, batch);
this._onBatchEnd.fire(batch);
}
@@ -376,7 +349,6 @@ export default class QueryRunner extends Disposable {
if (batchSet && !batchSet.resultSetSummaries[resultSet.id]) {
// Store the result set in the batch and emit that a result set has completed
batchSet.resultSetSummaries[resultSet.id] = resultSet;
this._eventEmitter.emit(EventType.RESULT_SET, resultSet);
this._onResultSet.fire(resultSet);
}
}
@@ -398,7 +370,7 @@ export default class QueryRunner extends Disposable {
this._planXml.resolve(e.resultSubset.rows[0][0].displayValue);
// fire query plan available event if execution is completed
if (result.resultSetSummary.complete) {
this._eventEmitter.emit(EventType.QUERY_PLAN_AVAILABLE, {
this._onQueryPlanAvailable.fire({
providerId: 'MSSQL',
fileUri: result.ownerUri,
planXml: planXmlString
@@ -425,7 +397,6 @@ export default class QueryRunner extends Disposable {
this._messages.push(message);
// Send the message to the results pane
this._eventEmitter.emit(EventType.MESSAGE, message);
this._onMessage.fire(message);
}
@@ -461,7 +432,7 @@ export default class QueryRunner extends Disposable {
return this._queryManagementService.initializeEdit(ownerUri, schemaName, objectName, objectType, rowLimit, queryString).then(result => {
// The query has started, so lets fire up the result pane
this._eventEmitter.emit(EventType.START);
this._onQueryStart.fire();
this._queryManagementService.registerRunner(this, ownerUri);
}, error => {
// Attempting to launch the query failed, show the error message
@@ -511,7 +482,7 @@ export default class QueryRunner extends Disposable {
}
public handleEditSessionReady(ownerUri: string, success: boolean, message: string): void {
this._eventEmitter.emit(EventType.EDIT_SESSION_READY, { ownerUri, success, message });
this._onEditSessionReady.fire({ ownerUri, success, message });
}
public updateCell(ownerUri: string, rowId: number, columnId: number, newValue: string): Thenable<azdata.EditUpdateCellResult> {

View File

@@ -7,7 +7,6 @@ import { Action, IActionItem, IActionRunner } from 'vs/base/common/actions';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { EditDataEditor } from 'sql/workbench/parts/editData/browser/editDataEditor';
import * as nls from 'vs/nls';
@@ -151,7 +150,7 @@ export class ChangeMaxRowsAction extends EditDataAction {
* Action item that handles the dropdown (combobox) that lists the avaliable number of row selections
* for an edit data session
*/
export class ChangeMaxRowsActionItem extends EventEmitter implements IActionItem {
export class ChangeMaxRowsActionItem implements IActionItem {
public actionRunner: IActionRunner;
public defaultRowCount: number;
@@ -167,7 +166,6 @@ export class ChangeMaxRowsActionItem extends EventEmitter implements IActionItem
private _editor: EditDataEditor,
@IContextViewService contextViewService: IContextViewService,
@IThemeService private _themeService: IThemeService) {
super();
this._options = ['200', '1000', '10000'];
this._currentOptionsIndex = 0;
this.toDispose = [];

View File

@@ -433,7 +433,7 @@ export class ProfilerEditor extends BaseEditor {
if (this._stateListener) {
this._stateListener.dispose();
}
this._stateListener = input.state.addChangeListener(e => this._onStateChange(e));
this._stateListener = input.state.onProfilerStateChange(e => this._onStateChange(e));
this._onStateChange({
isConnected: true,
isRunning: true,

View File

@@ -123,7 +123,7 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll
if (this._stateListener) {
this._stateListener.dispose();
}
this._stateListener = input.state.addChangeListener(e => this._onStateChange(e));
this._stateListener = input.state.onProfilerStateChange(e => this._onStateChange(e));
input.data.onRowCountChange(() => {
this._profilerTable.updateRowCount();
this._updateRowCountStatus();

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
export interface IProfilerStateChangedEvent {
isConnected?: boolean;
@@ -25,7 +25,6 @@ export interface INewProfilerState {
}
export class ProfilerState implements IDisposable {
private static _CHANGED_EVENT = 'changed';
private _isConnected: boolean;
private _isRunning: boolean;
@@ -33,7 +32,6 @@ export class ProfilerState implements IDisposable {
private _isStopped: boolean;
private _autoscroll: boolean;
private _isPanelCollapsed = true;
private _eventEmitter: EventEmitter;
public get isConnected(): boolean { return this._isConnected; }
public get isRunning(): boolean { return this._isRunning; }
@@ -42,16 +40,10 @@ export class ProfilerState implements IDisposable {
public get autoscroll(): boolean { return this._autoscroll; }
public get isPanelCollapsed(): boolean { return this._isPanelCollapsed; }
constructor() {
this._eventEmitter = new EventEmitter();
}
private readonly _onProfilerStateChange = new Emitter<IProfilerStateChangedEvent>();
public readonly onProfilerStateChange = this._onProfilerStateChange.event;
public dispose(): void {
this._eventEmitter.dispose();
}
public addChangeListener(listener: (e: IProfilerStateChangedEvent) => void): IDisposable {
return this._eventEmitter.addListener(ProfilerState._CHANGED_EVENT, listener);
}
public change(newState: INewProfilerState): void {
@@ -109,7 +101,7 @@ export class ProfilerState implements IDisposable {
}
if (somethingChanged) {
this._eventEmitter.emit(ProfilerState._CHANGED_EVENT, changeEvent);
this._onProfilerStateChange.fire(changeEvent);
}
}
}

View File

@@ -26,7 +26,6 @@ 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/parts/editableDropdown/browser/dropdown';
/**
@@ -428,7 +427,7 @@ export class ListDatabasesAction extends QueryTaskbarAction {
* Action item that handles the dropdown (combobox) that lists the available databases.
* Based off StartDebugActionItem.
*/
export class ListDatabasesActionItem extends EventEmitter implements IActionItem {
export class ListDatabasesActionItem implements IActionItem {
public static ID = 'listDatabaseQueryActionItem';
public actionRunner: IActionRunner;
@@ -450,7 +449,6 @@ export class ListDatabasesActionItem extends EventEmitter implements IActionItem
@IContextViewService contextViewProvider: IContextViewService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) {
super();
this._toDispose = [];
this._databaseListDropdown = $('.databaseListDropdown');
this._isInAccessibilityMode = this._configurationService.getValue('editor.accessibilitySupport') === 'on';

View File

@@ -6,7 +6,7 @@
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IInsightsConfigDetails } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
import QueryRunner, { EventType as QREvents } from 'sql/platform/query/common/queryRunner';
import QueryRunner from 'sql/platform/query/common/queryRunner';
import * as Utils from 'sql/platform/connection/common/utils';
import { IInsightsDialogModel } from 'sql/workbench/services/insights/common/insightsDialogService';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
@@ -141,12 +141,12 @@ export class InsightsDialogController {
}
private addQueryEventListeners(queryRunner: QueryRunner): void {
queryRunner.addListener(QREvents.COMPLETE, () => {
queryRunner.onQueryEnd(() => {
this.queryComplete().catch(error => {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), error);
});
});
queryRunner.addListener(QREvents.MESSAGE, message => {
queryRunner.onMessage(message => {
if (message.isError) {
this._errorMessageService.showDialog(Severity.Error, nls.localize("insightsError", "Insights error"), message.message);
}

View File

@@ -0,0 +1,253 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInsightsLabel, IInsightsConfigDetails } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
import { InsightsDialogModel } from 'sql/workbench/services/insights/common/insightsDialogModel';
import { isUndefinedOrNull } from 'vs/base/common/types';
import * as assert from 'assert';
suite('Insights Dialog Model Tests', () => {
test('does parse condition right', () => {
let insightsDialogModel = new InsightsDialogModel();
let label: IInsightsLabel = {
column: undefined,
state: [
{
condition: {
if: 'always'
},
color: 'green'
}
]
} as IInsightsLabel;
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', 'value1'],
['label2', 'value2'],
['label3', 'value3']
];
let result = insightsDialogModel.getListResources(0, 1);
for (let resource of result) {
assert.equal(resource.stateColor, 'green', 'always Condition did not return val as expected');
}
label.state = [
{
condition: {
if: 'equals',
equals: 'specific value'
},
color: 'green'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', 'specific value'],
['label2', 'value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[1].stateColor), true, 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[2].stateColor), true, 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'equals',
equals: 'specific value'
},
color: 'green'
},
{
condition: {
if: 'equals',
equals: 'specific value2'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', 'specific value'],
['label2', 'specific value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[1].stateColor, 'red', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[2].stateColor), true, 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'greaterThan',
equals: '2'
},
color: 'green'
},
{
condition: {
if: 'equals',
equals: 'specific value2'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', '3'],
['label2', 'specific value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[1].stateColor, 'red', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[2].stateColor), true, 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'greaterThanOrEquals',
equals: '2'
},
color: 'green'
},
{
condition: {
if: 'equals',
equals: 'specific value2'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', '2'],
['label2', 'specific value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[1].stateColor, 'red', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[2].stateColor), true, 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'lessThan',
equals: '8'
},
color: 'green'
},
{
condition: {
if: 'equals',
equals: 'specific value2'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', '5'],
['label2', 'specific value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[1].stateColor, 'red', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[2].stateColor), true, 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'lessThanOrEquals',
equals: '8'
},
color: 'green'
},
{
condition: {
if: 'equals',
equals: 'specific value2'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', '8'],
['label2', 'specific value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[1].stateColor, 'red', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[2].stateColor), true, 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'notEquals',
equals: '9'
},
color: 'green'
},
{
condition: {
if: 'equals',
equals: 'specific value2'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', '8'],
['label2', '9'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(isUndefinedOrNull(result[1].stateColor), true, 'always Condition did not return val as expected');
assert.equal(result[2].stateColor, 'green', 'always Condition did not return val as expected');
label.state = [
{
condition: {
if: 'notEquals',
equals: '9'
},
color: 'green'
},
{
condition: {
if: 'always'
},
color: 'red'
}
];
insightsDialogModel.insight = { label } as IInsightsConfigDetails;
insightsDialogModel.rows = [
['label1', '8'],
['label2', 'specific value2'],
['label3', 'value3']
];
result = insightsDialogModel.getListResources(0, 1);
assert.equal(result[0].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[1].stateColor, 'green', 'always Condition did not return val as expected');
assert.equal(result[2].stateColor, 'green', 'always Condition did not return val as expected');
});
});

View File

@@ -0,0 +1,235 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equal, fail } from 'assert';
import * as os from 'os';
import { resolveQueryFilePath } from 'sql/workbench/services/insights/common/insightsUtils';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { Workspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService';
import { TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { IExtensionHostDebugParams, IDebugParams, ParsedArgs } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
class TestEnvironmentService implements IWorkbenchEnvironmentService {
machineSettingsHome: string;
machineSettingsPath: string;
extensionDevelopmentLocationURI?: URI[];
constructor(private userEnv: { [key: string]: any }) {
}
get configuration(): IWindowConfiguration {
return {
userEnv: this.userEnv
} as IWindowConfiguration;
}
_serviceBrand: any;
args: ParsedArgs;
execPath: string;
cliPath: string;
appRoot: string;
userHome: string;
userDataPath: string;
appNameLong: string;
appQuality?: string;
appSettingsHome: string;
appSettingsPath: string;
appKeybindingsPath: string;
settingsSearchBuildId?: number;
settingsSearchUrl?: string;
globalStorageHome: string;
workspaceStorageHome: string;
backupHome: string;
backupWorkspacesPath: string;
untitledWorkspacesHome: URI;
isExtensionDevelopment: boolean;
disableExtensions: boolean | string[];
builtinExtensionsPath: string;
extensionsPath: string;
extensionTestsLocationURI?: URI;
debugExtensionHost: IExtensionHostDebugParams;
debugSearch: IDebugParams;
logExtensionHostCommunication: boolean;
isBuilt: boolean;
wait: boolean;
status: boolean;
log?: string;
logsPath: string;
verbose: boolean;
skipGettingStarted: boolean;
skipReleaseNotes: boolean;
skipAddToRecentlyOpened: boolean;
mainIPCHandle: string;
sharedIPCHandle: string;
nodeCachedDataDir?: string;
installSourcePath: string;
disableUpdates: boolean;
disableCrashReporter: boolean;
driverHandle?: string;
driverVerbose: boolean;
}
suite('Insights Utils tests', function () {
let testRootPath: string;
let queryFileDir: string;
let queryFilePath: string;
suiteSetup(async () => {
// Create test file - just needs to exist for verifying the path resolution worked correctly
testRootPath = path.join(os.tmpdir(), 'adstests');
queryFileDir = getRandomTestPath(testRootPath, 'insightsutils');
await pfs.mkdirp(queryFileDir);
queryFilePath = path.join(queryFileDir, 'test.sql');
await pfs.writeFile(queryFilePath, '');
});
test('resolveQueryFilePath resolves path correctly with fully qualified path', async () => {
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({}),
undefined,
undefined,
new TestContextService(),
undefined);
const resolvedPath = await resolveQueryFilePath(queryFilePath, new TestContextService(), configurationResolverService);
equal(resolvedPath, queryFilePath);
});
test('resolveQueryFilePath resolves path correctly with workspaceRoot var and non-empty workspace containing file', async () => {
// Create mock context service with our test folder added as a workspace folder for resolution
const contextService = new TestContextService(
new Workspace(
'TestWorkspace',
[toWorkspaceFolder(URI.file(queryFileDir))]
));
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({}),
undefined,
undefined,
contextService,
undefined);
const resolvedPath = await resolveQueryFilePath(path.join('${workspaceRoot}', 'test.sql'), contextService, configurationResolverService);
equal(resolvedPath, queryFilePath);
});
test('resolveQueryFilePath throws with workspaceRoot var and non-empty workspace not containing file', async (done) => {
const tokenizedPath = path.join('${workspaceRoot}', 'test.sql');
// Create mock context service with a folder NOT containing our test file to verify it returns original path
const contextService = new TestContextService(
new Workspace(
'TestWorkspace',
[toWorkspaceFolder(URI.file(os.tmpdir()))])
);
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({}),
undefined,
undefined,
contextService,
undefined);
try {
await resolveQueryFilePath(tokenizedPath, contextService, configurationResolverService);
fail('Should have thrown');
}
catch (e) {
done();
}
});
test('resolveQueryFilePath throws with workspaceRoot var and empty workspace', async (done) => {
const tokenizedPath = path.join('${workspaceRoot}', 'test.sql');
// Create mock context service with an empty workspace
const contextService = new TestContextService(
new Workspace(
'TestWorkspace'));
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({}),
undefined,
undefined,
contextService,
undefined);
try {
await resolveQueryFilePath(tokenizedPath, contextService, configurationResolverService);
fail('Should have thrown');
}
catch (e) {
done();
}
});
test('resolveQueryFilePath resolves path correctly with env var and empty workspace', async () => {
const contextService = new TestContextService(
new Workspace('TestWorkspace'));
// Create mock window service with env variable containing test folder for resolution
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({ TEST_PATH: queryFileDir }),
undefined,
undefined,
undefined,
undefined);
const resolvedPath = await resolveQueryFilePath(path.join('${env:TEST_PATH}', 'test.sql'), contextService, configurationResolverService);
equal(resolvedPath, queryFilePath);
});
test('resolveQueryFilePath resolves path correctly with env var and non-empty workspace', async () => {
const contextService = new TestContextService(
new Workspace('TestWorkspace', [toWorkspaceFolder(URI.file(os.tmpdir()))]));
// Create mock window service with env variable containing test folder for resolution
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({ TEST_PATH: queryFileDir }),
undefined,
undefined,
undefined,
undefined);
const resolvedPath = await resolveQueryFilePath(path.join('${env:TEST_PATH}', 'test.sql'), contextService, configurationResolverService);
equal(resolvedPath, queryFilePath);
});
test('resolveQueryFilePath throws if invalid param var specified', async (done) => {
const invalidPath = path.join('${INVALID}', 'test.sql');
const configurationResolverService = new ConfigurationResolverService(
undefined,
new TestEnvironmentService({}),
undefined,
undefined,
undefined,
undefined);
try {
await resolveQueryFilePath(invalidPath, new TestContextService(), configurationResolverService);
fail('Should have thrown');
} catch (e) {
done();
}
});
suiteTeardown(() => {
// Clean up our test files
return pfs.rimraf(testRootPath, pfs.RimRafMode.MOVE);
});
});

View File

@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InsightsDialogController } from 'sql/workbench/services/insights/node/insightsDialogController';
import { InsightsDialogModel } from 'sql/workbench/services/insights/common/insightsDialogModel';
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { ConnectionManagementService } from 'sql/platform/connection/common/connectionManagementService';
import { IInsightsConfigDetails } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import * as azdata from 'azdata';
import { equal } from 'assert';
import { Mock, MockBehavior, It } from 'typemoq';
import { TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { Emitter } from 'vs/base/common/event';
const testData: string[][] = [
['1', '2', '3', '4'],
['5', '6', '7', '8']
];
const testColumns: string[] = [
'col1',
'col2'
];
suite('Insights Dialog Controller Tests', () => {
test('updates correctly with good input', done => {
let model = new InsightsDialogModel();
let { runner, complete } = getPrimedQueryRunner(testData, testColumns);
let instMoq = Mock.ofType(InstantiationService, MockBehavior.Strict);
instMoq.setup(x => x.createInstance(It.isValue(QueryRunner), It.isAny()))
.returns(() => runner);
let connMoq = Mock.ofType(ConnectionManagementService, MockBehavior.Strict, {}, {}, new TestStorageService());
connMoq.setup(x => x.connect(It.isAny(), It.isAny()))
.returns(() => Promise.resolve(undefined));
let controller = new InsightsDialogController(
model,
undefined,
undefined,
instMoq.object,
connMoq.object,
undefined,
undefined,
undefined
);
let profile: IConnectionProfile = {
connectionName: 'newname',
serverName: 'server',
databaseName: 'database',
userName: 'user',
password: '',
authenticationType: '',
savePassword: true,
groupFullName: '',
groupId: '',
getOptionsKey: () => '',
matches: undefined,
providerName: '',
saveProfile: true,
id: '',
options: {}
};
controller.update(<IInsightsConfigDetails>{ query: 'query' }, profile).then(() => {
// Once we update the controller, listen on when it changes the model and verify the data it
// puts in is correct
model.onDataChange(() => {
for (let i = 0; i < testData.length; i++) {
for (let j = 0; j < testData[i].length; j++) {
equal(testData[i][j], model.rows[i][j]);
}
}
done();
});
// Fake the query Runner telling the controller the query is complete
complete();
});
});
});
interface IPrimedQueryRunner {
runner: QueryRunner;
complete: () => void;
}
/**
* Returns a mock of query runner than will recreate what a query runner does to return data
*/
function getPrimedQueryRunner(data: string[][], columns: string[]): IPrimedQueryRunner {
const emitter = new Emitter<string>();
const querymock = Mock.ofType(QueryRunner, MockBehavior.Strict);
querymock.setup(x => x.onQueryEnd).returns(x => emitter.event);
querymock.setup(x => x.onMessage).returns(x => new Emitter<azdata.IResultMessage>().event);
querymock.setup(x => x.batchSets).returns(x => {
return <Array<azdata.BatchSummary>>[
{
id: 0,
resultSetSummaries: [
{
columnInfo: <Array<azdata.IDbColumn>>columns.map(c => { return { columnName: c }; }),
id: 0,
rowCount: data.length
}
]
}
];
});
querymock.setup(x => x.getQueryRows(It.isAnyNumber(), It.isAnyNumber(), It.isAnyNumber(), It.isAnyNumber()))
.returns(x => Promise.resolve(<azdata.QueryExecuteSubsetResult>{
resultSubset: <azdata.ResultSetSubset>{
rowCount: data.length,
rows: data.map(r => r.map(c => { return { displayValue: c }; }))
}
}));
querymock.setup(x => x.runQuery(It.isAnyString())).returns(x => Promise.resolve());
const complete = () => {
emitter.fire('time');
};
return {
runner: querymock.object,
complete
};
}

View File

@@ -8,7 +8,7 @@ import { nb, QueryExecuteSubsetResult, IDbColumn, BatchSummary, IResultMessage,
import { localize } from 'vs/nls';
import * as strings from 'vs/base/common/strings';
import { FutureInternal, ILanguageMagic, notebookConstants } from 'sql/workbench/parts/notebook/models/modelInterfaces';
import QueryRunner, { EventType } from 'sql/platform/query/common/queryRunner';
import QueryRunner from 'sql/platform/query/common/queryRunner';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import Severity from 'vs/base/common/severity';
@@ -304,18 +304,18 @@ class SqlKernel extends Disposable implements nb.IKernel {
}
private addQueryEventListeners(queryRunner: QueryRunner): void {
this._register(queryRunner.addListener(EventType.COMPLETE, () => {
this._register(queryRunner.onQueryEnd(() => {
this.queryComplete().catch(error => {
this._errorMessageService.showDialog(Severity.Error, sqlKernelError, error);
});
}));
this._register(queryRunner.addListener(EventType.MESSAGE, message => {
this._register(queryRunner.onMessage(message => {
// TODO handle showing a messages output (should be updated with all messages, only changing 1 output in total)
if (this._future) {
this._future.handleMessage(message);
}
}));
this._register(queryRunner.addListener(EventType.BATCH_COMPLETE, batch => {
this._register(queryRunner.onBatchEnd(batch => {
if (this._future) {
this._future.handleBatchEnd(batch);
}