diff --git a/tensorboard/components/experimental/plugin_lib/BUILD b/tensorboard/components/experimental/plugin_lib/BUILD new file mode 100644 index 0000000000..d4528b9095 --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/BUILD @@ -0,0 +1,32 @@ +package(default_visibility = ["//tensorboard:internal"]) + +load("//tensorboard/defs:web.bzl", "tf_web_library") + +licenses(["notice"]) # Apache 2.0 + +tf_web_library( + name = "guest_internals", + srcs = [ + "plugin-guest.ts", + ], + path = "/tf-plugin-lib", + deps = [ + "//tensorboard/components/experimental/plugin_util:message", + ], +) + +# TODO(psybuzz): figure out how this tf_web_library can be used to create +# maybe a NPM package. +tf_web_library( + name = "plugin_lib", + srcs = [ + "runs.ts", + "tf-plugin-lib.html", + ], + path = "/tf-plugin-lib", + visibility = ["//visibility:public"], + deps = [ + ":guest_internals", + "//tensorboard/components/experimental/plugin_util:message", + ], +) diff --git a/tensorboard/components/experimental/plugin_lib/plugin-guest.ts b/tensorboard/components/experimental/plugin_lib/plugin-guest.ts new file mode 100644 index 0000000000..8bf480f655 --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/plugin-guest.ts @@ -0,0 +1,55 @@ +/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +namespace tb_plugin.lib.DO_NOT_USE_INTERNAL { + /** + * This code is part of a public bundle provided to plugin authors, + * and runs within an IFrame to setup communication with TensorBoard's frame. + */ + if (!window.parent) { + throw Error( + 'The library must run within a TensorBoard iframe-based plugin.' + ); + } + + const channel = new MessageChannel(); + const ipc = new IPC(channel.port1); + channel.port1.start(); + + const VERSION = 'experimental'; + window.parent.postMessage(`${VERSION}.bootstrap`, '*', [channel.port2]); + + // Only export for testability. + export const _guestIPC = ipc; + + /** + * Sends a message to the parent frame. + * @return Promise that resolves with a payload from parent in response to this message. + * + * @example + * const someList = await sendMessage('v1.some.type.parent.understands'); + * // do fun things with someList. + */ + export const sendMessage = _guestIPC.sendMessage.bind(_guestIPC); + + /** + * Subscribes a callback to a message with particular type. + */ + export const listen = _guestIPC.listen.bind(_guestIPC); + + /** + * Unsubscribes a callback to a message. + */ + export const unlisten = _guestIPC.unlisten.bind(_guestIPC); +} // namespace tb_plugin.lib.DO_NOT_USE_INTERNAL diff --git a/tensorboard/components/experimental/plugin_util/test/iframe.ts b/tensorboard/components/experimental/plugin_lib/runs.ts similarity index 65% rename from tensorboard/components/experimental/plugin_util/test/iframe.ts rename to tensorboard/components/experimental/plugin_lib/runs.ts index c1c979687c..e672d67806 100644 --- a/tensorboard/components/experimental/plugin_util/test/iframe.ts +++ b/tensorboard/components/experimental/plugin_lib/runs.ts @@ -12,8 +12,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ +namespace tb_plugin.lib.runs { + export async function getRuns() { + return tb_plugin.lib.DO_NOT_USE_INTERNAL.sendMessage( + 'experimental.GetRuns' + ); + } -import {sendMessage, listen, unlisten, _guestIPC} from '../plugin-guest.js'; - -const win = window as any; -win.test = {sendMessage, listen, unlisten, _guestIPC}; + export function setOnRunsChanged(callback: (runs: string[]) => void | void) { + return tb_plugin.lib.DO_NOT_USE_INTERNAL.listen( + 'experimental.RunsChanged', + callback + ); + } +} diff --git a/tensorboard/components/experimental/plugin_lib/test/BUILD b/tensorboard/components/experimental/plugin_lib/test/BUILD new file mode 100644 index 0000000000..5493fc842b --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/test/BUILD @@ -0,0 +1,31 @@ +package( + default_testonly = True, + default_visibility = ["//tensorboard:internal"], +) + +load("//tensorboard/defs:web.bzl", "tf_web_library", "tf_web_test") + +licenses(["notice"]) # Apache 2.0 + +tf_web_test( + name = "test", + src = "/tf-plugin-lib/test/test.html", + web_library = ":test_web_library", +) + +tf_web_library( + name = "test_web_library", + testonly = True, + srcs = [ + "test.html", + "test.ts", + "testable-iframe.html", + ], + path = "/tf-plugin-lib/test", + deps = [ + "//tensorboard/components/experimental/plugin_lib", + "//tensorboard/components/experimental/plugin_util:plugin_host", + "//tensorboard/components/tf_backend", + "//tensorboard/components/tf_imports:web_component_tester", + ], +) diff --git a/tensorboard/components/experimental/plugin_lib/test/test.html b/tensorboard/components/experimental/plugin_lib/test/test.html new file mode 100644 index 0000000000..69a9862a1c --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/test/test.html @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/tensorboard/components/experimental/plugin_lib/test/test.ts b/tensorboard/components/experimental/plugin_lib/test/test.ts new file mode 100644 index 0000000000..b6e79f0d58 --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/test/test.ts @@ -0,0 +1,91 @@ +/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +async function createIframe(): Promise { + return new Promise((resolve) => { + const iframe = document.createElement('iframe') as HTMLIFrameElement; + document.body.appendChild(iframe); + iframe.src = './testable-iframe.html'; + iframe.onload = () => resolve(iframe); + }); +} + +describe('plugin lib integration', () => { + const {expect} = chai; + + beforeEach(async function() { + this.sandbox = sinon.sandbox.create({useFakeServer: true}); + this.sandbox.server.respondImmediately = true; + this.iframe = await createIframe(); + this.lib = (this.iframe.contentWindow as any).plugin_lib; + }); + + afterEach(function() { + document.body.removeChild(this.iframe); + this.sandbox.restore(); + }); + + describe('tb_plugin.lib.run', () => { + describe('#getRuns', () => { + it('returns list of runs', async function() { + this.sandbox + .stub(tf_backend.runsStore, 'getRuns') + .returns(['foo', 'bar', 'baz']); + + const runs = await this.lib.runs.getRuns(); + expect(runs).to.deep.equal(['foo', 'bar', 'baz']); + }); + }); + describe('#setOnRunsChanged', () => { + it('lets plugins subscribe to runs change', async function() { + const runsChanged = this.sandbox.stub(); + const promise = new Promise((resolve) => { + this.lib.runs.setOnRunsChanged(resolve); + }).then(runsChanged); + this.sandbox.server.respondWith([ + 200, + {'Content-Type': 'application/json'}, + '["foo", "bar"]', + ]); + + await tf_backend.runsStore.refresh(); + await promise; + + expect(runsChanged).to.have.been.calledOnce; + expect(runsChanged).to.have.been.calledWith(['foo', 'bar']); + }); + it('lets plugins unsubscribe to runs change', async function() { + const runsChanged = this.sandbox.stub(); + const promise = new Promise((resolve) => { + this.lib.runs.setOnRunsChanged(resolve); + }).then(runsChanged); + this.lib.runs.setOnRunsChanged(); + this.sandbox.server.respondWith([ + 200, + {'Content-Type': 'application/json'}, + '["foo", "bar"]', + ]); + + await tf_backend.runsStore.refresh(); + + // Await another message to ensure the iframe processed the next message + // (if any). + await this.lib.DO_NOT_USE_INTERNAL.sendMessage('foo'); + + expect(runsChanged).to.not.have.been.called; + }); + }); + }); +}); diff --git a/tensorboard/components/experimental/plugin_lib/test/testable-iframe.html b/tensorboard/components/experimental/plugin_lib/test/testable-iframe.html new file mode 100644 index 0000000000..a71873734a --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/test/testable-iframe.html @@ -0,0 +1,20 @@ + + + diff --git a/tensorboard/components/experimental/plugin_lib/tf-plugin-lib.html b/tensorboard/components/experimental/plugin_lib/tf-plugin-lib.html new file mode 100644 index 0000000000..ed9aa45eef --- /dev/null +++ b/tensorboard/components/experimental/plugin_lib/tf-plugin-lib.html @@ -0,0 +1,20 @@ + + + + + diff --git a/tensorboard/components/experimental/plugin_util/BUILD b/tensorboard/components/experimental/plugin_util/BUILD index 622dfeef6e..ccd6969f3b 100644 --- a/tensorboard/components/experimental/plugin_util/BUILD +++ b/tensorboard/components/experimental/plugin_util/BUILD @@ -5,35 +5,35 @@ load("//tensorboard/defs:web.bzl", "tf_web_library") licenses(["notice"]) # Apache 2.0 tf_web_library( - name = "plugin_host", + name = "message", srcs = [ - "plugin-host.html", - "plugin-host.ts", - ], - path = "/tf-plugin", - deps = [ - ":plugin_lib", - "//tensorboard/components/tf_backend", + "message.html", + "message.ts", ], + path = "/tf-plugin-util", ) tf_web_library( - name = "plugin_lib", + name = "host_internals", srcs = [ - "message.ts", + "plugin-host-ipc.html", + "plugin-host-ipc.ts", + ], + path = "/tf-plugin-util", + deps = [ + ":message", ], - path = "/tf-plugin", ) tf_web_library( - name = "plugin_guest", + name = "plugin_host", srcs = [ - "plugin-guest.html", - "plugin-guest.ts", + "plugin-host.html", + "runs-host-impl.ts", ], - path = "/tf-plugin", - visibility = ["//visibility:public"], + path = "/tf-plugin-util", deps = [ - ":plugin_lib", + ":host_internals", + "//tensorboard/components/tf_backend", ], ) diff --git a/tensorboard/components/experimental/plugin_util/plugin-guest.html b/tensorboard/components/experimental/plugin_util/message.html similarity index 94% rename from tensorboard/components/experimental/plugin_util/plugin-guest.html rename to tensorboard/components/experimental/plugin_util/message.html index 807d96f6e7..bd98b715f5 100644 --- a/tensorboard/components/experimental/plugin_util/plugin-guest.html +++ b/tensorboard/components/experimental/plugin_util/message.html @@ -14,5 +14,4 @@ See the License for the specific language governing permissions and limitations under the License. --> - diff --git a/tensorboard/components/experimental/plugin_util/message.ts b/tensorboard/components/experimental/plugin_util/message.ts index 805f2f2752..bc132b8919 100644 --- a/tensorboard/components/experimental/plugin_util/message.ts +++ b/tensorboard/components/experimental/plugin_util/message.ts @@ -17,100 +17,101 @@ limitations under the License. * This file defines utilities shared by TensorBoard (plugin host) and the * dynamic plugin library, used by plugin authors. */ - -export type PayloadType = - | null - | undefined - | string - | string[] - | boolean - | boolean[] - | number - | number[] - | object - | object[]; - -export interface Message { - type: string; - id: number; - payload: PayloadType; - error: string | null; - isReply: boolean; -} - -export type MessageType = string; -export type MessageCallback = (payload: any) => any; - -interface PromiseResolver { - resolve: (data: any) => void; - reject: (error: Error) => void; -} - -export class IPC { - private id = 0; - private readonly responseWaits = new Map(); - private readonly listeners = new Map(); - - constructor(private port: MessagePort) { - this.port.addEventListener('message', (event) => this.onMessage(event)); +namespace tb_plugin.lib.DO_NOT_USE_INTERNAL { + export type PayloadType = + | null + | undefined + | string + | string[] + | boolean + | boolean[] + | number + | number[] + | object + | object[]; + + export interface Message { + type: string; + id: number; + payload: PayloadType; + error: string | null; + isReply: boolean; } - listen(type: MessageType, callback: MessageCallback) { - this.listeners.set(type, callback); - } + export type MessageType = string; + export type MessageCallback = (payload: any) => any; - unlisten(type: MessageType) { - this.listeners.delete(type); + interface PromiseResolver { + resolve: (data: any) => void; + reject: (error: Error) => void; } - private async onMessage(event: MessageEvent) { - const message = JSON.parse(event.data) as Message; - const callback = this.listeners.get(message.type); - - if (message.isReply) { - if (!this.responseWaits.has(message.id)) return; - const {id, payload, error} = message; - const {resolve, reject} = this.responseWaits.get(id); - this.responseWaits.delete(id); - if (error) { - reject(new Error(error)); - } else { - resolve(payload); - } - return; + export class IPC { + private id = 0; + private readonly responseWaits = new Map(); + private readonly listeners = new Map(); + + constructor(private port: MessagePort) { + this.port.addEventListener('message', (event) => this.onMessage(event)); + } + + listen(type: MessageType, callback: MessageCallback) { + this.listeners.set(type, callback); + } + + unlisten(type: MessageType) { + this.listeners.delete(type); } - let payload = null; - let error = null; - if (this.listeners.has(message.type)) { + private async onMessage(event: MessageEvent) { + const message = JSON.parse(event.data) as Message; const callback = this.listeners.get(message.type); - try { - const result = await callback(message.payload); - payload = result; - } catch (e) { - error = e; + + if (message.isReply) { + if (!this.responseWaits.has(message.id)) return; + const {id, payload, error} = message; + const {resolve, reject} = this.responseWaits.get(id); + this.responseWaits.delete(id); + if (error) { + reject(new Error(error)); + } else { + resolve(payload); + } + return; + } + + let payload = null; + let error = null; + if (this.listeners.has(message.type)) { + const callback = this.listeners.get(message.type); + try { + const result = await callback(message.payload); + payload = result; + } catch (e) { + error = e; + } } + const replyMessage: Message = { + type: message.type, + id: message.id, + payload, + error, + isReply: true, + }; + this.postMessage(replyMessage); } - const replyMessage: Message = { - type: message.type, - id: message.id, - payload, - error, - isReply: true, - }; - this.postMessage(replyMessage); - } - private postMessage(message: Message) { - this.port.postMessage(JSON.stringify(message)); - } + private postMessage(message: Message) { + this.port.postMessage(JSON.stringify(message)); + } - sendMessage(type: MessageType, payload: PayloadType): Promise { - const id = this.id++; - const message: Message = {type, id, payload, error: null, isReply: false}; - this.postMessage(message); - return new Promise((resolve, reject) => { - this.responseWaits.set(id, {resolve, reject}); - }); + sendMessage(type: MessageType, payload: PayloadType): Promise { + const id = this.id++; + const message: Message = {type, id, payload, error: null, isReply: false}; + this.postMessage(message); + return new Promise((resolve, reject) => { + this.responseWaits.set(id, {resolve, reject}); + }); + } } -} +} // namespace tb_plugin.lib.DO_NOT_USE_INTERNAL diff --git a/tensorboard/components/experimental/plugin_util/plugin-guest.ts b/tensorboard/components/experimental/plugin_util/plugin-guest.ts deleted file mode 100644 index 5837473518..0000000000 --- a/tensorboard/components/experimental/plugin_util/plugin-guest.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -==============================================================================*/ -import {IPC, Message} from './message.js'; - -/** - * This code is part of a public bundle provided to plugin authors, - * and runs within an IFrame to setup communication with TensorBoard's frame. - */ -if (!window.parent) { - throw Error('The library must run within a TensorBoard iframe-based plugin.'); -} - -const channel = new MessageChannel(); -const ipc = new IPC(channel.port1); -channel.port1.start(); - -const VERSION = 'experimental'; -window.parent.postMessage(`${VERSION}.bootstrap`, '*', [channel.port2]); - -// Only export for testability. -export const _guestIPC = ipc; - -/** - * Sends a message to the parent frame. - * @return Promise that resolves with a payload from parent in response to this message. - * - * @example - * const someList = await sendMessage('v1.some.type.parent.understands'); - * // do fun things with someList. - */ -export const sendMessage = _guestIPC.sendMessage.bind(_guestIPC); - -/** - * Subscribes a callback to a message with particular type. - */ -export const listen = _guestIPC.listen.bind(_guestIPC); - -/** - * Unsubscribes a callback to a message. - */ -export const unlisten = _guestIPC.unlisten.bind(_guestIPC); diff --git a/tensorboard/components/experimental/plugin_util/plugin-host-ipc.html b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.html new file mode 100644 index 0000000000..4f0bcbe83f --- /dev/null +++ b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.html @@ -0,0 +1,18 @@ + + + diff --git a/tensorboard/components/experimental/plugin_util/plugin-host-ipc.ts b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.ts new file mode 100644 index 0000000000..8618cdd22c --- /dev/null +++ b/tensorboard/components/experimental/plugin_util/plugin-host-ipc.ts @@ -0,0 +1,96 @@ +/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +namespace tb_plugin.host { + const portIPCs = new Set(); + const VERSION = 'experimental'; + const listeners = new Map< + lib.DO_NOT_USE_INTERNAL.MessageType, + lib.DO_NOT_USE_INTERNAL.MessageCallback + >(); + + // TODO(@psybuzz): replace this and the port cleanup logic in broadcast() with + // a MutationObserver to notify us when iframes disconnect. + const ipcToFrame = new Map(); + + // The initial Window-level listener is needed to bootstrap only. + // All further communication is done over MessagePorts. + window.addEventListener('message', (event) => { + if (event.data !== `${VERSION}.bootstrap`) return; + const port = event.ports[0]; + if (!port) return; + const frame = event.source ? event.source.frameElement : null; + if (!frame) return; + onBootstrap(port, frame as HTMLIFrameElement); + }); + + function onBootstrap(port: MessagePort, frame: HTMLIFrameElement) { + const portIPC = new lib.DO_NOT_USE_INTERNAL.IPC(port); + portIPCs.add(portIPC); + ipcToFrame.set(portIPC, frame); + port.start(); + + for (const [type, callback] of listeners) { + portIPC.listen(type, callback); + } + } + + /** + * Sends a message to all frames. Individual frames decide whether or not to + * listen. + * @return Promise that resolves with a list of payloads from each plugin's + * response (or null) to the message. + * + * @example + * const someList = await broadcast('v1.some.type.guest.understands'); + * // do fun things with someList. + */ + export function broadcast( + type: lib.DO_NOT_USE_INTERNAL.MessageType, + payload: lib.DO_NOT_USE_INTERNAL.PayloadType + ): Promise { + for (const ipc of portIPCs) { + if (!ipcToFrame.get(ipc).isConnected) { + portIPCs.delete(ipc); + ipcToFrame.delete(ipc); + } + } + + const promises = [...portIPCs].map((ipc) => ipc.sendMessage(type, payload)); + return Promise.all(promises); + } + + /** + * Subscribes to messages of a type specified for all frames. + */ + export function listen( + type: lib.DO_NOT_USE_INTERNAL.MessageType, + callback: lib.DO_NOT_USE_INTERNAL.MessageCallback + ) { + listeners.set(type, callback); + for (const ipc of portIPCs) { + ipc.listen(type, callback); + } + } + + /** + * Unsubscribes to messages of a type specified for all frames. + */ + export function unlisten(type: lib.DO_NOT_USE_INTERNAL.MessageType) { + listeners.delete(type); + for (const ipc of portIPCs) { + ipc.unlisten(type); + } + } +} // namespace tb_plugin.host diff --git a/tensorboard/components/experimental/plugin_util/plugin-host.html b/tensorboard/components/experimental/plugin_util/plugin-host.html index d1b6f3ae89..c9c40556f3 100644 --- a/tensorboard/components/experimental/plugin_util/plugin-host.html +++ b/tensorboard/components/experimental/plugin_util/plugin-host.html @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - - + + diff --git a/tensorboard/components/experimental/plugin_util/plugin-host.ts b/tensorboard/components/experimental/plugin_util/plugin-host.ts deleted file mode 100644 index 1de5c554cb..0000000000 --- a/tensorboard/components/experimental/plugin_util/plugin-host.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* Copyright 2019 The TensorFlow Authors. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -==============================================================================*/ -import {IPC, MessageType, PayloadType, MessageCallback} from './message.js'; - -const portIPCs = new Set(); -const VERSION = 'experimental'; -const listeners = new Map(); - -// TODO(@psybuzz): replace this and the port cleanup logic in broadcast() with -// a MutationObserver to notify us when iframes disconnect. -const ipcToFrame = new Map(); - -// The initial Window-level listener is needed to bootstrap only. -// All further communication is done over MessagePorts. -window.addEventListener('message', (event) => { - if (event.data !== `${VERSION}.bootstrap`) return; - const port = event.ports[0]; - if (!port) return; - const frame = event.source ? event.source.frameElement : null; - if (!frame) return; - onBootstrap(port, frame as HTMLIFrameElement); -}); - -function onBootstrap(port: MessagePort, frame: HTMLIFrameElement) { - const portIPC = new IPC(port); - portIPCs.add(portIPC); - ipcToFrame.set(portIPC, frame); - port.start(); - - for (const [type, callback] of listeners) { - portIPC.listen(type, callback); - } -} - -function _broadcast( - type: MessageType, - payload: PayloadType -): Promise { - for (const ipc of portIPCs) { - if (!ipcToFrame.get(ipc).isConnected) { - portIPCs.delete(ipc); - ipcToFrame.delete(ipc); - } - } - - const promises = [...portIPCs].map((ipc) => ipc.sendMessage(type, payload)); - return Promise.all(promises); -} - -function _listen(type: MessageType, callback: MessageCallback) { - listeners.set(type, callback); - for (const ipc of portIPCs) { - ipc.listen(type, callback); - } -} - -function _unlisten(type: MessageType) { - listeners.delete(type); - for (const ipc of portIPCs) { - ipc.unlisten(type); - } -} - -export const broadcast = _broadcast; -export const listen = _listen; -export const unlisten = _unlisten; - -namespace tf_plugin { - /** - * Sends a message to all frames. Individual frames decide whether or not to - * listen. - * @return Promise that resolves with a list of payloads from each plugin's - * response (or null) to the message. - * - * @example - * const someList = await broadcast('v1.some.type.guest.understands'); - * // do fun things with someList. - */ - export const broadcast = _broadcast; - /** - * Subscribes to messages of a type specified for all frames. - */ - export const listen = _listen; - /** - * Unsubscribes to messages of a type specified for all frames. - */ - export const unlisten = _unlisten; -} // namespace tf_plugin diff --git a/tensorboard/components/experimental/plugin_util/runs-host-impl.ts b/tensorboard/components/experimental/plugin_util/runs-host-impl.ts new file mode 100644 index 0000000000..2d6020421b --- /dev/null +++ b/tensorboard/components/experimental/plugin_util/runs-host-impl.ts @@ -0,0 +1,27 @@ +/* Copyright 2017 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/** + * Implements run related plugin APIs. + */ +tb_plugin.host.listen('experimental.GetRuns', () => { + return tf_backend.runsStore.getRuns(); +}); + +tf_backend.runsStore.addListener(() => { + return tb_plugin.host.broadcast( + 'experimental.RunsChanged', + tf_backend.runsStore.getRuns() + ); +}); diff --git a/tensorboard/components/experimental/plugin_util/test/BUILD b/tensorboard/components/experimental/plugin_util/test/BUILD index eea69f1d29..e598af9bd5 100644 --- a/tensorboard/components/experimental/plugin_util/test/BUILD +++ b/tensorboard/components/experimental/plugin_util/test/BUILD @@ -4,56 +4,25 @@ package( ) load("//tensorboard/defs:web.bzl", "tf_web_library", "tf_web_test") -load("//tensorboard/defs:vulcanize.bzl", "tensorboard_html_binary") licenses(["notice"]) # Apache 2.0 tf_web_test( name = "test", - src = "/tf-plugin/test/test_binary.html", + src = "/tf-plugin-util/test/tests.html", web_library = ":test_web_library", ) -# HACK: specifying tensorboard_html_binary on tf_web_test causes certain -# environment to throw exception but wrapping tensorbard_html_binary with -# tf_web_library seems to be okay. tf_web_library( name = "test_web_library", - srcs = [ - ":test_binary.html", - ], - path = "/tf-plugin/test", - deps = [ - ":test_lib", - "//tensorboard/components/tf_imports:web_component_tester", - ], -) - -tensorboard_html_binary( - name = "test_binary", - # Disable advanced optimization to prevent check for WebComponentTester - # gobals. - compilation_level = "SIMPLE", - # Requires for compiling `import`s away. - compile = True, - input_path = "/tf-plugin/test/tests.html", - output_path = "/tf-plugin/test/test_binary.html", - deps = [ - ":test_lib", - ], -) - -tf_web_library( - name = "test_lib", srcs = [ "iframe.html", - "iframe.ts", "plugin-test.ts", "tests.html", ], - path = "/tf-plugin/test", + path = "/tf-plugin-util/test", deps = [ - "//tensorboard/components/experimental/plugin_util:plugin_guest", + "//tensorboard/components/experimental/plugin_lib:plugin_lib", "//tensorboard/components/experimental/plugin_util:plugin_host", "//tensorboard/components/tf_imports:web_component_tester", ], diff --git a/tensorboard/components/experimental/plugin_util/test/iframe.html b/tensorboard/components/experimental/plugin_util/test/iframe.html index 9aff72251d..d0e75f91e2 100644 --- a/tensorboard/components/experimental/plugin_util/test/iframe.html +++ b/tensorboard/components/experimental/plugin_util/test/iframe.html @@ -1,4 +1,3 @@ - - - - + + + diff --git a/tensorboard/components/experimental/plugin_util/test/plugin-test.ts b/tensorboard/components/experimental/plugin_util/test/plugin-test.ts index b56a30f1d8..ed663b0711 100644 --- a/tensorboard/components/experimental/plugin_util/test/plugin-test.ts +++ b/tensorboard/components/experimental/plugin_util/test/plugin-test.ts @@ -12,10 +12,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ -import * as pluginHost from '../plugin-host.js'; -import {Message} from '../message.js'; -namespace tf_plugin.test { +namespace tb_plugin.lib.DO_NOT_USE_INTERNAL { const {expect} = chai; const template = document.getElementById( 'iframe-template' @@ -60,7 +58,7 @@ namespace tf_plugin.test { this.destUnlisten = this.guestWindow.test.unlisten; this.destSendMessage = this.guestWindow.test.sendMessage; this.srcSendMessage = (type, payload) => { - return pluginHost + return tb_plugin.host .broadcast(type, payload) .then(([result]) => result); }; @@ -70,10 +68,10 @@ namespace tf_plugin.test { spec: 'guest (src) to host (dest)', beforeEachFunc: function() { this.destWindow = window; - this.destListen = pluginHost.listen; - this.destUnlisten = pluginHost.unlisten; + this.destListen = tb_plugin.host.listen; + this.destUnlisten = tb_plugin.host.unlisten; this.destSendMessage = (type, payload) => { - return pluginHost + return tb_plugin.host .broadcast(type, payload) .then(([result]) => result); }; @@ -220,4 +218,4 @@ namespace tf_plugin.test { }); }); }); -} // namespace tf_plugin.test +} // namespace tf_plugin.lib.DO_NOT_USE_INTERNAL diff --git a/tensorboard/components/experimental/plugin_util/test/tests.html b/tensorboard/components/experimental/plugin_util/test/tests.html index 76946a9b0e..20a4d13677 100644 --- a/tensorboard/components/experimental/plugin_util/test/tests.html +++ b/tensorboard/components/experimental/plugin_util/test/tests.html @@ -20,4 +20,4 @@ - + diff --git a/tensorboard/components/tf_tensorboard/BUILD b/tensorboard/components/tf_tensorboard/BUILD index 4bffafc1a7..ec583c8122 100644 --- a/tensorboard/components/tf_tensorboard/BUILD +++ b/tensorboard/components/tf_tensorboard/BUILD @@ -23,6 +23,7 @@ tf_web_library( "//tensorboard/components/tf_imports:polymer", "//tensorboard/components/tf_paginated_view", "//tensorboard/components/tf_storage", + "//tensorboard/components/experimental/plugin_util:plugin_host", "@com_google_fonts_roboto", "@org_polymer_iron_icons", "@org_polymer_paper_button", diff --git a/tensorboard/components/tf_tensorboard/tf-tensorboard.html b/tensorboard/components/tf_tensorboard/tf-tensorboard.html index 36adac7827..a984ae7ec7 100644 --- a/tensorboard/components/tf_tensorboard/tf-tensorboard.html +++ b/tensorboard/components/tf_tensorboard/tf-tensorboard.html @@ -31,6 +31,7 @@ +