diff --git a/samples/sqlservices/src/controllers/button.html b/samples/sqlservices/src/controllers/button.html new file mode 100644 index 0000000000..f7185f4d9a --- /dev/null +++ b/samples/sqlservices/src/controllers/button.html @@ -0,0 +1,18 @@ + + +
+
+ + + + + diff --git a/samples/sqlservices/src/controllers/counter.html b/samples/sqlservices/src/controllers/counter.html new file mode 100644 index 0000000000..9dc092f6cf --- /dev/null +++ b/samples/sqlservices/src/controllers/counter.html @@ -0,0 +1,21 @@ + + +
+ +
+ +
Total Counts: 0
+ + + diff --git a/samples/sqlservices/src/controllers/mainController.ts b/samples/sqlservices/src/controllers/mainController.ts index be8c2dcb25..e2dc1ab0f5 100644 --- a/samples/sqlservices/src/controllers/mainController.ts +++ b/samples/sqlservices/src/controllers/mainController.ts @@ -9,6 +9,8 @@ import * as sqlops from 'sqlops'; import * as Utils from '../utils'; import * as vscode from 'vscode'; import SplitPropertiesPanel from './splitPropertiesPanel'; +import * as fs from 'fs'; +import * as path from 'path'; /** * The main controller class that initializes the extension @@ -33,6 +35,8 @@ export default class MainController implements vscode.Disposable { } public activate(): Promise { + const buttonHtml = fs.readFileSync(path.join(__dirname, 'button.html')).toString(); + const counterHtml = fs.readFileSync(path.join(__dirname, 'counter.html')).toString(); this.registerSqlServicesModelView(); this.registerSplitPanelModelView(); @@ -40,12 +44,12 @@ export default class MainController implements vscode.Disposable { vscode.window.showInformationMessage(`Clicked from profile ${profile.serverName}.${profile.databaseName}`); }); - vscode.commands.registerCommand('sqlservices.openDialog', () => { + vscode.commands.registerCommand('sqlservices.openDialog', () => { this.openDialog(); }); - vscode.commands.registerCommand('sqlservices.openEditor', () => { - this.openEditor(); + vscode.commands.registerCommand('sqlservices.openEditor', () => { + this.openEditor(buttonHtml, counterHtml); }); return Promise.resolve(true); @@ -69,51 +73,51 @@ export default class MainController implements vscode.Disposable { dialog.customButtons = [customButton1, customButton2]; tab1.registerContent(async (view) => { let inputBox = view.modelBuilder.inputBox() - .withProperties({ - //width: 300 - }) - .component(); + .withProperties({ + //width: 300 + }) + .component(); let inputBox2 = view.modelBuilder.inputBox() - .component(); + .component(); let checkbox = view.modelBuilder.checkBox() - .withProperties({ - label: 'Copy-only backup' - }) - .component(); + .withProperties({ + label: 'Copy-only backup' + }) + .component(); checkbox.onChanged(e => { - console.info("inputBox.enabled " + inputBox.enabled); - inputBox.enabled = !inputBox.enabled; + console.info("inputBox.enabled " + inputBox.enabled); + inputBox.enabled = !inputBox.enabled; }); let button = view.modelBuilder.button() - .withProperties({ - label: '+' - }).component(); + .withProperties({ + label: '+' + }).component(); let button3 = view.modelBuilder.button() - .withProperties({ - label: '-' + .withProperties({ + label: '-' - }).component(); + }).component(); let button2 = view.modelBuilder.button() - .component(); + .component(); button.onDidClick(e => { - inputBox2.value = 'Button clicked'; + inputBox2.value = 'Button clicked'; }); let dropdown = view.modelBuilder.dropDown() - .withProperties({ - value: 'Full', - values: ['Full', 'Differential', 'Transaction Log'] - }) - .component(); + .withProperties({ + value: 'Full', + values: ['Full', 'Differential', 'Transaction Log'] + }) + .component(); let f = 0; inputBox.onTextChanged((params) => { - vscode.window.showInformationMessage(inputBox.value); + vscode.window.showInformationMessage(inputBox.value); f = f + 1; - inputBox2.value=f.toString(); + inputBox2.value = f.toString(); }); dropdown.onValueChanged((params) => { - vscode.window.showInformationMessage(inputBox2.value); - inputBox.value = dropdown.value; + vscode.window.showInformationMessage(inputBox2.value); + inputBox.value = dropdown.value; }); let radioButton = view.modelBuilder.radioButton() .withProperties({ @@ -121,68 +125,84 @@ export default class MainController implements vscode.Disposable { name: 'radioButtonOptions', label: 'Option 1', checked: true - //width: 300 - }).component(); + //width: 300 + }).component(); let radioButton2 = view.modelBuilder.radioButton() .withProperties({ value: 'option2', name: 'radioButtonOptions', label: 'Option 2' - //width: 300 - }).component(); + //width: 300 + }).component(); let flexRadioButtonsModel = view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column', alignItems: 'left', justifyContent: 'space-evenly', height: 50 - }).withItems([ - radioButton, radioButton2] + }).withItems([ + radioButton, radioButton2] , { flex: '1 1 50%' }).component(); let formModel = view.modelBuilder.formContainer() - .withFormItems([{ - component: inputBox, - title: 'Backup name' + .withFormItems([{ + component: inputBox, + title: 'Backup name' }, { - component: inputBox2, - title: 'Recovery model' + component: inputBox2, + title: 'Recovery model' }, { - component:dropdown, - title: 'Backup type' + component: dropdown, + title: 'Backup type' }, { - component: checkbox, - title: '' + component: checkbox, + title: '' }, { - component: inputBox2, - title: 'Backup files', - actions: [button, button3] + component: inputBox2, + title: 'Backup files', + actions: [button, button3] }, { component: flexRadioButtonsModel, title: 'Options' }], { - horizontal:false, - width: 500, - componentWidth: 400 - }).component(); + horizontal: false, + width: 500, + componentWidth: 400 + }).component(); await view.initializeModel(formModel); }); sqlops.window.modelviewdialog.openDialog(dialog); } - private openEditor(): void { + private openEditor(html1: string, html2: string): void { let editor = sqlops.workspace.createModelViewEditor('Test Editor view'); editor.registerContent(async view => { - let inputBox = view.modelBuilder.inputBox() - .withValidation(component => component.value !== 'valid') + let count = 0; + let webview1 = view.modelBuilder.webView() + .withProperties({ + html: html1 + }) .component(); - let formModel = view.modelBuilder.formContainer() - .withFormItems([{ - component: inputBox, - title: 'Enter anything but "valid"' - }]).component(); - await view.initializeModel(formModel); + let webview2 = view.modelBuilder.webView() + .withProperties({ + html: html2 + }) + .component(); + webview1.onMessage((params) => { + count++; + webview2.message = count; + }); + + let flexModel = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + alignItems: 'left' + }).withItems([ + webview1, webview2 + ], { flex: '1 1 50%' }) + .component(); + await view.initializeModel(flexModel); }); editor.openEditor(); } diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index 85f19d4bdf..d25de4266b 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -11,6 +11,7 @@ import DropDownComponent from './dropdown.component'; import ButtonComponent from './button.component'; import CheckBoxComponent from './checkbox.component'; import RadioButtonComponent from './radioButton.component'; +import WebViewComponent from './webview.component'; import TextComponent from './text.component'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -40,5 +41,8 @@ registerComponentType(CHECKBOX_COMPONENT, ModelComponentTypes.CheckBox, CheckBox export const RADIOBUTTON_COMPONENT = 'radiobutton-component'; registerComponentType(RADIOBUTTON_COMPONENT, ModelComponentTypes.RadioButton, RadioButtonComponent); +export const WEBVIEW_COMPONENT = 'webview-component'; +registerComponentType(WEBVIEW_COMPONENT, ModelComponentTypes.WebView, WebViewComponent); + export const TEXT_COMPONENT = 'text-component'; registerComponentType(TEXT_COMPONENT, ModelComponentTypes.Text, TextComponent); diff --git a/src/sql/parts/modelComponents/interfaces.ts b/src/sql/parts/modelComponents/interfaces.ts index a735e49933..a96750305c 100644 --- a/src/sql/parts/modelComponents/interfaces.ts +++ b/src/sql/parts/modelComponents/interfaces.ts @@ -63,7 +63,8 @@ export enum ComponentEventType { PropertiesChanged, onDidChange, onDidClick, - validityChanged + validityChanged, + onMessage } export interface IModelStore { diff --git a/src/sql/parts/modelComponents/webview.component.ts b/src/sql/parts/modelComponents/webview.component.ts new file mode 100644 index 0000000000..9d9980df0e --- /dev/null +++ b/src/sql/parts/modelComponents/webview.component.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./webview'; +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver, + ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList +} from '@angular/core'; + +import * as sqlops from 'sqlops'; +import Event, { Emitter } from 'vs/base/common/event'; +import { Webview } from 'vs/workbench/parts/html/browser/webview'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { Parts } from 'vs/workbench/services/part/common/partService'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; + +@Component({ + template: '', + selector: 'webview-component' +}) +export default class WebViewComponent extends ComponentBase implements IComponent, OnDestroy { + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + + private _webview: Webview; + private _onMessage = new Emitter(); + private _renderedHtml: string; + + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) private _el: ElementRef) { + super(changeRef); + } + + ngOnInit(): void { + this.baseInit(); + this._createWebview(); + this._register(addDisposableListener(window, EventType.RESIZE, e => { + this.layout(); + })); + } + + private _createWebview(): void { + this._webview = this._register(new Webview(this._el.nativeElement, + this._commonService.partService.getContainer(Parts.EDITOR_PART), + this._commonService.themeService, + this._commonService.environmentService, + this._commonService.contextViewService, + undefined, + undefined, + { + allowScripts: true, + enableWrappedPostMessage: true + } + )); + + + this._register(this._webview.onMessage(e => { + this._onEventEmitter.fire({ + eventType: ComponentEventType.onMessage, + args: e + }); + })); + + this._webview.style(this._commonService.themeService.getTheme()); + this.setHtml(); + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + /// Webview Functions + + private setHtml(): void { + if (this._webview && this.html) { + this._renderedHtml = this.html; + this._webview.contents = this._renderedHtml; + this._webview.layout(); + } + } + + private sendMessage(): void { + if (this._webview && this.message) { + this._webview.sendMessage(this.message); + } + } + + /// IComponent implementation + + public layout(): void { + this._webview.layout(); + } + + public setLayout(layout: any): void { + // TODO allow configuring the look and feel + this.layout(); + } + + public setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + if (this.html !== this._renderedHtml) { + this.setHtml(); + } + this.sendMessage(); + } + + // CSS-bound properties + + public get message(): any { + return this.getPropertyOrDefault((props) => props.message, undefined); + } + + public set message(newValue: any) { + this.setPropertyFromUI((properties, message) => { properties.message = message; }, newValue); + } + + public get html(): string { + return this.getPropertyOrDefault((props) => props.html, undefined); + } + + public set html(newValue: string) { + this.setPropertyFromUI((properties, html) => { properties.html = html; }, newValue); + } +} diff --git a/src/sql/parts/modelComponents/webview.css b/src/sql/parts/modelComponents/webview.css new file mode 100644 index 0000000000..33bf076256 --- /dev/null +++ b/src/sql/parts/modelComponents/webview.css @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +webview-component { + height: 100%; + width : 100%; + display: block; +} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index ba5fbaee39..8d8bf3ac73 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -22,11 +22,12 @@ declare module 'sqlops' { inputBox(): ComponentBuilder; checkBox(): ComponentBuilder; radioButton(): ComponentBuilder; + webView(): ComponentBuilder; text(): ComponentBuilder; button(): ComponentBuilder; dropDown(): ComponentBuilder; - dashboardWidget(widgetId: string): ComponentBuilder; - dashboardWebview(webviewId: string): ComponentBuilder; + dashboardWidget(widgetId: string): ComponentBuilder; + dashboardWebview(webviewId: string): ComponentBuilder; formContainer(): FormBuilder; } @@ -273,6 +274,11 @@ declare module 'sqlops' { editable?: boolean; } + export interface WebViewProperties { + message?: any; + html?: string; + } + export interface ButtonProperties { label?: string; } @@ -308,16 +314,22 @@ declare module 'sqlops' { onValueChanged: vscode.Event; } + export interface WebViewComponent extends Component { + html: string; + message: any; + onMessage: vscode.Event; + } + export interface ButtonComponent extends Component { label: string; onDidClick: vscode.Event; } - export interface WidgetComponent extends Component { + export interface DashboardWidgetComponent extends Component { widgetId: string; } - export interface WebviewComponent extends Component { + export interface DashboardWebviewComponent extends Component { webviewId: string; } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index aa302f4966..d486cacf2d 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -72,6 +72,7 @@ export enum ModelComponentTypes { Button, CheckBox, RadioButton, + WebView, Text, DashboardWidget, DashboardWebview, @@ -95,7 +96,8 @@ export enum ComponentEventType { PropertiesChanged, onDidChange, onDidClick, - validityChanged + validityChanged, + onMessage } export interface IComponentEventArgs { diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index 05d629825b..afc3bfa8f8 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -80,6 +80,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } + webView(): sqlops.ComponentBuilder { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl = this.getComponentBuilder(new WebViewWrapper(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); @@ -94,16 +101,16 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } - dashboardWidget(widgetId: string): sqlops.ComponentBuilder { + dashboardWidget(widgetId: string): sqlops.ComponentBuilder { let id = this.getNextComponentId(); - let builder = this.getComponentBuilder(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id), id); + 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 { + dashboardWebview(webviewId: string): sqlops.ComponentBuilder { let id = this.getNextComponentId(); - let builder: ComponentBuilderImpl = this.getComponentBuilder(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWebview, id), id); + let builder: ComponentBuilderImpl = this.getComponentBuilder(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWebview, id), id); this._componentBuilders.set(id, builder); return builder; } @@ -523,6 +530,34 @@ class CheckBoxWrapper extends ComponentWrapper implements sqlops.CheckBoxCompone } } +class WebViewWrapper extends ComponentWrapper implements sqlops.WebViewComponent { + + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.WebView, id); + this.properties = {}; + this._emitterMap.set(ComponentEventType.onMessage, new Emitter()); + } + + public get message(): any { + return this.properties['message']; + } + public set message(v: any) { + this.setProperty('message', v); + } + + public get html(): string { + return this.properties['html']; + } + public set html(v: string) { + this.setProperty('html', v); + } + + public get onMessage(): vscode.Event { + let emitter = this._emitterMap.get(ComponentEventType.onMessage); + return emitter && emitter.event; + } +} + class RadioButtonWrapper extends ComponentWrapper implements sqlops.RadioButtonComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {