/*--------------------------------------------------------------------------------------------- * 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 { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; import { Emitter } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import * as vscode from 'vscode'; import * as sqlops from 'sqlops'; import { SqlMainContext, ExtHostModelViewShape, MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { IItemConfig, ModelComponentTypes, IComponentShape, IComponentEventArgs, ComponentEventType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IActionDescriptor } from 'vs/editor/standalone/browser/standaloneCodeEditor'; class ModelBuilderImpl implements sqlops.ModelBuilder { private nextComponentId: number; private readonly _componentBuilders = new Map>(); constructor(private readonly _proxy: MainThreadModelViewShape, private readonly _handle: number) { this.nextComponentId = 0; } navContainer(): sqlops.ContainerBuilder { let id = this.getNextComponentId(); let container: ContainerBuilderImpl = new ContainerBuilderImpl(this._proxy, this._handle, ModelComponentTypes.NavContainer, id); this._componentBuilders.set(id, container); return container; } flexContainer(): sqlops.FlexBuilder { let id = this.getNextComponentId(); let container: ContainerBuilderImpl = new ContainerBuilderImpl(this._proxy, this._handle, ModelComponentTypes.FlexContainer, id); this._componentBuilders.set(id, container); return container; } formContainer(): sqlops.FormBuilder { let id = this.getNextComponentId(); let container = new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id); this._componentBuilders.set(id, container); return container; } card(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new CardWrapper(this._proxy, this._handle, id), id); this._componentBuilders.set(id, builder); return builder; } inputBox(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new InputBoxWrapper(this._proxy, this._handle, id), id); this._componentBuilders.set(id, builder); return builder; } radioButton(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new RadioButtonWrapper(this._proxy, this._handle, id), id); this._componentBuilders.set(id, builder); return builder; } checkBox(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new CheckBoxWrapper(this._proxy, this._handle, id), id); this._componentBuilders.set(id, builder); return builder; } button(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new ButtonWrapper(this._proxy, this._handle, id), id); this._componentBuilders.set(id, builder); return builder; } dropDown(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new DropDownWrapper(this._proxy, this._handle, id), id); this._componentBuilders.set(id, builder); return builder; } dashboardWidget(widgetId: string): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder = this.getComponentBuilder(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id), id); this._componentBuilders.set(id, builder); return builder; } dashboardWebview(webviewId: string): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWebview, id), id); this._componentBuilders.set(id, builder); return builder; } getComponentBuilder(component: ComponentWrapper, id: string): ComponentBuilderImpl { let componentBuilder: ComponentBuilderImpl = new ComponentBuilderImpl(component); this._componentBuilders.set(id, componentBuilder); return componentBuilder; } handleEvent(componentId: string, eventArgs: IComponentEventArgs): void { let eventHandler = this._componentBuilders.get(componentId); if (eventHandler) { eventHandler.handleEvent(eventArgs); } } public runCustomValidations(componentId: string): boolean { let component = this._componentBuilders.get(componentId).componentWrapper(); return component.runCustomValidations(); } private getNextComponentId(): string { return `component${this._handle}_${this.nextComponentId++}`; } } interface IWithEventHandler { handleEvent(eventArgs: IComponentEventArgs): void; } class ComponentBuilderImpl implements sqlops.ComponentBuilder, IWithEventHandler { constructor(protected _component: ComponentWrapper) { _component.registerEvent(); } component(): T { return this._component; } componentWrapper(): ComponentWrapper { return this._component; } withProperties(properties: U): sqlops.ComponentBuilder { this._component.properties = properties; return this; } withValidation(validation: (component: T) => boolean): sqlops.ComponentBuilder { this._component.customValidations.push(validation); return this; } handleEvent(eventArgs: IComponentEventArgs) { this._component.onEvent(eventArgs); } } class GenericComponentBuilder extends ComponentBuilderImpl { constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { super(new ComponentWrapper(proxy, handle, type, id)); } } class ContainerBuilderImpl extends ComponentBuilderImpl implements sqlops.ContainerBuilder { constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { super(new ComponentWrapper(proxy, handle, type, id)); } withLayout(layout: TLayout): sqlops.ContainerBuilder { this._component.layout = layout; return this; } withItems(components: sqlops.Component[], itemLayout?: TItemLayout): sqlops.ContainerBuilder { this._component.itemConfigs = components.map(item => { let componentWrapper = item as ComponentWrapper; return new InternalItemConfig(componentWrapper, itemLayout); }); return this; } } class FormContainerBuilder extends ContainerBuilderImpl implements sqlops.FormBuilder { withFormItems(components: sqlops.FormComponent[], itemLayout?: sqlops.FormItemLayout): sqlops.ContainerBuilder { this._component.itemConfigs = components.map(item => { return this.convertToItemConfig(item, itemLayout); }); components.forEach(formItem => { this.addComponentActions(formItem, itemLayout); }); return this; } private convertToItemConfig(formComponent: sqlops.FormComponent, itemLayout?: sqlops.FormItemLayout): InternalItemConfig { let componentWrapper = formComponent.component as ComponentWrapper; let actions: string[] = undefined; if (formComponent.actions) { actions = formComponent.actions.map(action => { let actionComponentWrapper = action as ComponentWrapper; return actionComponentWrapper.id; }); } return new InternalItemConfig(componentWrapper, Object.assign({}, itemLayout, { title: formComponent.title, actions: actions, isFormComponent: true })); } private addComponentActions(formComponent: sqlops.FormComponent, itemLayout?: sqlops.FormItemLayout): void { if (formComponent.actions) { formComponent.actions.forEach(component => { let componentWrapper = component as ComponentWrapper; this._component.addItem(componentWrapper, itemLayout); }); } } addFormItems(formComponents: Array, itemLayout?: sqlops.FormItemLayout): void { formComponents.forEach(formComponent => { this.addFormItem(formComponent, itemLayout); }); } addFormItem(formComponent: sqlops.FormComponent, itemLayout?: sqlops.FormItemLayout): void { let itemImpl = this.convertToItemConfig(formComponent, itemLayout); this._component.addItem(formComponent.component as ComponentWrapper, itemImpl.config); this.addComponentActions(formComponent, itemLayout); } } class InternalItemConfig { constructor(private _component: ComponentWrapper, public config: any) { } public toIItemConfig(): IItemConfig { return { config: this.config, componentShape: this._component.toComponentShape() }; } public get component(): sqlops.Component { return this._component; } } class ComponentWrapper implements sqlops.Component { public properties: { [key: string]: any } = {}; public layout: any; public itemConfigs: InternalItemConfig[]; public customValidations: ((component: ThisType) => boolean)[] = []; private _valid: boolean = true; private _onValidityChangedEmitter = new Emitter(); public readonly onValidityChanged = this._onValidityChangedEmitter.event; private _onErrorEmitter = new Emitter(); public readonly onError: vscode.Event = this._onErrorEmitter.event; protected _emitterMap = new Map>(); constructor(protected readonly _proxy: MainThreadModelViewShape, protected readonly _handle: number, protected _type: ModelComponentTypes, protected _id: string ) { this.properties = {}; this.itemConfigs = []; } public get id(): string { return this._id; } public get type(): ModelComponentTypes { return this._type; } public get items(): sqlops.Component[] { return this.itemConfigs.map(itemConfig => itemConfig.component); } public get enabled(): boolean { return this.properties['enabled']; } public set enabled(value: boolean) { this.setProperty('enabled', value); } public toComponentShape(): IComponentShape { return { id: this.id, type: this.type, layout: this.layout, properties: this.properties, itemConfigs: this.itemConfigs ? this.itemConfigs.map(item => item.toIItemConfig()) : undefined }; } public clearItems(): Thenable { this.itemConfigs = []; return this._proxy.$clearContainer(this._handle, this.id); } public addItems(items: Array, itemLayout?: any): void { for (let item of items) { this.addItem(item, itemLayout); } } public addItem(item: sqlops.Component, itemLayout?: any): void { let itemImpl = item as ComponentWrapper; if (!itemImpl) { throw new Error(nls.localize('unknownComponentType', 'Unkown component type. Must use ModelBuilder to create objects')); } let config = new InternalItemConfig(itemImpl, itemLayout); this.itemConfigs.push(config); this._proxy.$addToContainer(this._handle, this.id, config.toIItemConfig()).then(undefined, this.handleError); } public setLayout(layout: any): Thenable { return this._proxy.$setLayout(this._handle, this.id, layout); } public updateProperties(): Thenable { return this.notifyPropertyChanged(); } protected notifyPropertyChanged(): Thenable { return this._proxy.$setProperties(this._handle, this._id, this.properties).then(() => true); } public registerEvent(): Thenable { return this._proxy.$registerEvent(this._handle, this._id).then(() => true); } public onEvent(eventArgs: IComponentEventArgs) { if (eventArgs && eventArgs.eventType === ComponentEventType.PropertiesChanged) { this.properties = eventArgs.args; } else if (eventArgs && eventArgs.eventType === ComponentEventType.validityChanged) { this._valid = eventArgs.args; } else if (eventArgs) { let emitter = this._emitterMap.get(eventArgs.eventType); if (emitter) { emitter.fire(eventArgs.args); } } } protected async setProperty(key: string, value: any): Promise { if (!this.properties[key] || this.properties[key] !== value) { // Only notify the front end if a value has been updated this.properties[key] = value; return this.notifyPropertyChanged(); } return Promise.resolve(true); } private handleError(err: Error): void { this._onErrorEmitter.fire(err); } public runCustomValidations(): boolean { let isValid = true; try { this.customValidations.forEach(validation => { if (!validation(this)) { isValid = false; } }); } catch (e) { isValid = false; } return isValid; } public validate() { return this._proxy.$validate(this._handle, this._id); } public get valid(): boolean { return this._valid; } } class ContainerWrapper extends ComponentWrapper implements sqlops.Container { constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { super(proxy, handle, type, id); } } class CardWrapper extends ComponentWrapper implements sqlops.CardComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.Card, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onDidClick, new Emitter()); } public get label(): string { return this.properties['label']; } public set label(l: string) { this.setProperty('label', l); } public get value(): string { return this.properties['value']; } public set value(v: string) { this.setProperty('value', v); } public get actions(): sqlops.ActionDescriptor[] { return this.properties['actions']; } public set actions(a: sqlops.ActionDescriptor[]) { this.setProperty('actions', a); } public get onDidActionClick(): vscode.Event { let emitter = this._emitterMap.get(ComponentEventType.onDidClick); return emitter && emitter.event; } } class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.InputBox, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onDidChange, new Emitter()); } public get value(): string { return this.properties['value']; } public set value(v: string) { this.setProperty('value', v); } public get ariaLabel(): string { return this.properties['ariaLabel']; } public set ariaLabel(v: string) { this.setProperty('ariaLabel', v); } public get placeHolder(): string { return this.properties['placeHolder']; } public set placeHolder(v: string) { this.setProperty('placeHolder', v); } public get height(): number { return this.properties['height']; } public set height(v: number) { this.setProperty('height', v); } public get width(): number { return this.properties['width']; } public set width(v: number) { this.setProperty('width', v); } public get inputType(): sqlops.InputBoxInputType { return this.properties['inputType']; } public set inputType(v: sqlops.InputBoxInputType) { this.setProperty('inputType', v); } public get onTextChanged(): vscode.Event { let emitter = this._emitterMap.get(ComponentEventType.onDidChange); return emitter && emitter.event; } } class CheckBoxWrapper extends ComponentWrapper implements sqlops.CheckBoxComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.CheckBox, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onDidChange, new Emitter()); } public get checked(): boolean { return this.properties['checked']; } public set checked(v: boolean) { this.setProperty('checked', v); } public get label(): string { return this.properties['label']; } public set label(v: string) { this.setProperty('label', v); } public get onChanged(): vscode.Event { let emitter = this._emitterMap.get(ComponentEventType.onDidChange); return emitter && emitter.event; } } class RadioButtonWrapper extends ComponentWrapper implements sqlops.RadioButtonComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.RadioButton, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onDidClick, new Emitter()); } public get name(): string { return this.properties['name']; } public set name(v: string) { this.setProperty('name', v); } public get label(): string { return this.properties['label']; } public set label(v: string) { this.setProperty('label', v); } public get value(): string { return this.properties['value']; } public set value(v: string) { this.setProperty('value', v); } public get checked(): boolean { return this.properties['checked']; } public set checked(v: boolean) { this.setProperty('checked', v); } public get onDidClick(): vscode.Event { let emitter = this._emitterMap.get(ComponentEventType.onDidClick); return emitter && emitter.event; } } class DropDownWrapper extends ComponentWrapper implements sqlops.DropDownComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.DropDown, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onDidChange, new Emitter()); } public get value(): string { return this.properties['value']; } public set value(v: string) { this.setProperty('value', v); } public get values(): string[] { return this.properties['values']; } public set values(v: string[]) { this.setProperty('values', v); } public get onValueChanged(): vscode.Event { let emitter = this._emitterMap.get(ComponentEventType.onDidChange); return emitter && emitter.event; } } class ButtonWrapper extends ComponentWrapper implements sqlops.ButtonComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.Button, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onDidClick, new Emitter()); } public get label(): string { return this.properties['label']; } public set label(v: string) { this.setProperty('label', v); } public get onDidClick(): vscode.Event { let emitter = this._emitterMap.get(ComponentEventType.onDidClick); return emitter && emitter.event; } } class ModelViewImpl implements sqlops.ModelView { public onClosedEmitter = new Emitter(); private _onValidityChangedEmitter = new Emitter(); public readonly onValidityChanged = this._onValidityChangedEmitter.event; private _modelBuilder: ModelBuilderImpl; private _component: sqlops.Component; constructor( private readonly _proxy: MainThreadModelViewShape, private readonly _handle: number, private readonly _connection: sqlops.connection.Connection, private readonly _serverInfo: sqlops.ServerInfo ) { this._modelBuilder = new ModelBuilderImpl(this._proxy, this._handle); } public get onClosed(): vscode.Event { return this.onClosedEmitter.event; } public get connection(): sqlops.connection.Connection { return deepClone(this._connection); } public get serverInfo(): sqlops.ServerInfo { return deepClone(this._serverInfo); } public get modelBuilder(): sqlops.ModelBuilder { return this._modelBuilder; } public get valid(): boolean { return this._component.valid; } public handleEvent(componentId: string, eventArgs: IComponentEventArgs): void { this._modelBuilder.handleEvent(componentId, eventArgs); } public initializeModel(component: T): Thenable { component.onValidityChanged(valid => this._onValidityChangedEmitter.fire(valid)); this._component = component; let componentImpl = component as ComponentWrapper; if (!componentImpl) { return Promise.reject(nls.localize('unknownConfig', 'Unkown component configuration, must use ModelBuilder to create a configuration object')); } return this._proxy.$initializeModel(this._handle, componentImpl.toComponentShape()); } public validate(): Thenable { return this._proxy.$validate(this._handle, this._component.id); } public runCustomValidations(componentId: string): boolean { return this._modelBuilder.runCustomValidations(componentId); } } export class ExtHostModelView implements ExtHostModelViewShape { private readonly _proxy: MainThreadModelViewShape; private readonly _modelViews = new Map(); private readonly _handlers = new Map void>(); constructor( mainContext: IMainContext ) { this._proxy = mainContext.getProxy(SqlMainContext.MainThreadModelView); } $onClosed(handle: number): void { const view = this._modelViews.get(handle); view.onClosedEmitter.fire(); this._modelViews.delete(handle); } $registerProvider(widgetId: string, handler: (webview: sqlops.ModelView) => void): void { this._handlers.set(widgetId, handler); this._proxy.$registerProvider(widgetId); } $registerWidget(handle: number, id: string, connection: sqlops.connection.Connection, serverInfo: sqlops.ServerInfo): void { let view = new ModelViewImpl(this._proxy, handle, connection, serverInfo); this._modelViews.set(handle, view); this._handlers.get(id)(view); } $handleEvent(handle: number, componentId: string, eventArgs: IComponentEventArgs): void { const view = this._modelViews.get(handle); if (view) { view.handleEvent(componentId, eventArgs); } } $runCustomValidations(handle: number, componentId: string): Thenable { const view = this._modelViews.get(handle); return Promise.resolve(view.runCustomValidations(componentId)); } }