diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000..77ed90c0a1 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": false, + "arrowParens": "always" +} diff --git a/.travis.yml b/.travis.yml index 8f1de9f44d..d2b7ec82ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,6 +55,7 @@ install: # TensorBoard deps. - pip install futures==3.1.1 - pip install grpcio==1.6.3 + - yarn install --ignore-engines # Uninstall older Travis numpy to avoid upgrade-in-place issues. - pip uninstall -y numpy - | @@ -78,6 +79,8 @@ before_script: # Do a fail-fast check for Python syntax errors or undefined names. # Use the comment '# noqa: ' to suppress. - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + # Lint frontend code + - yarn lint # Lint .yaml docs files. Use '# yamllint disable-line rule:foo' to suppress. - yamllint -c docs/.yamllint docs docs/.yamllint # Make sure that IPython notebooks have valid Markdown. diff --git a/package.json b/package.json index d208a4fc53..28a6bfae56 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "scripts": { "postinstall": "ngc -p angular-metadata.tsconfig.json", "build": "bazel build //...", - "test": "ibazel test //..." + "test": "ibazel test //...", + "lint": "prettier --check 'tensorboard/**/*.'{css,html,js,ts}", + "fix-lint": "prettier --write 'tensorboard/**/*.'{css,html,js,ts}" }, "repository": { "type": "git", @@ -35,6 +37,7 @@ "@bazel/ibazel": "^0.10.3", "@bazel/typescript": "^0.34.0", "@types/node": "^12.6.8", + "prettier": "1.18.2", "typescript": "~3.4.5" }, "dependencies": { diff --git a/tensorboard/components/tensor_widget/tensor-widget.ts b/tensorboard/components/tensor_widget/tensor-widget.ts index 6dbd88bf1b..053cadd80f 100644 --- a/tensorboard/components/tensor_widget/tensor-widget.ts +++ b/tensorboard/components/tensor_widget/tensor-widget.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ -import {TensorWidget, TensorWidgetOptions, TensorView} from "./types"; +import {TensorWidget, TensorWidgetOptions, TensorView} from './types'; /** * Create an instance of tensor widiget. @@ -24,8 +24,11 @@ import {TensorWidget, TensorWidgetOptions, TensorView} from "./types"; * @returns An instance of a single-tensor tensor widget. */ export function tensorWidget( - rootElement: HTMLDivElement, tensor: TensorView, - options?: TensorWidgetOptions): TensorWidget { + rootElement: HTMLDivElement, + tensor: TensorView, + options?: TensorWidgetOptions +): TensorWidget { throw new Error( - 'tensorWidget() factory method has not been implemented yet.'); + 'tensorWidget() factory method has not been implemented yet.' + ); } diff --git a/tensorboard/components/tensor_widget/types.ts b/tensorboard/components/tensor_widget/types.ts index 0699251e43..30ea0add4e 100644 --- a/tensorboard/components/tensor_widget/types.ts +++ b/tensorboard/components/tensor_widget/types.ts @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ -import {BaseTensorHealthPill} from "./health-pill-types"; +import {BaseTensorHealthPill} from './health-pill-types'; /** The basic specifications of a tensor. */ export interface TensorSpec { @@ -46,7 +46,7 @@ export interface TensorView { * being a non-negative integer. * @return The value of the element at the specified indices. */ - get: (...indices: number[]) => Promise; + get: (...indices: number[]) => Promise; /** * Get a view of the underlying tensor with the specified @@ -64,9 +64,18 @@ export interface TensorView { * tensor. */ export type SlicedValues = - boolean|boolean[]|boolean[][]|boolean[][][]| - number|number[]|number[][]|number[][][]| - string|string[]|string[][]|string[][][]; + | boolean + | boolean[] + | boolean[][] + | boolean[][][] + | number + | number[] + | number[][] + | number[][][] + | string + | string[] + | string[][] + | string[][][]; /** * A data structure that keeps track of how an n-dimensional array (tensor) @@ -100,7 +109,7 @@ export interface TensorViewSlicingSpec { * - The `dim` field is the 0-based dimension index. * - The `index` is the 0-based index for the selected slice. */ - slicingDimsAndIndices: Array<{dim: number, index: number}>; + slicingDimsAndIndices: Array<{dim: number; index: number}>; /** * Which dimensions are used for viewing (i.e., rendered in the diff --git a/tensorboard/components/tensorboard.html b/tensorboard/components/tensorboard.html index 90a8215ccc..77df476ddd 100644 --- a/tensorboard/components/tensorboard.html +++ b/tensorboard/components/tensorboard.html @@ -1,4 +1,4 @@ - + - + TensorBoard - - - - - - - + + + + + + + - + + diff --git a/tensorboard/components/tf_backend/backend.ts b/tensorboard/components/tf_backend/backend.ts index 8f53e6a9b1..21ea3c1657 100644 --- a/tensorboard/components/tf_backend/backend.ts +++ b/tensorboard/components/tf_backend/backend.ts @@ -13,85 +13,83 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { + export type RunToTag = { + [run: string]: string[]; + }; -export type RunToTag = { - [run: string]: string[]; -}; - -export interface Datum { - wall_time: Date; - step: number; -} + export interface Datum { + wall_time: Date; + step: number; + } -// An object that encapsulates an alert issued by the debugger. This alert is -// sent by debugging libraries after bad values (NaN, +/- Inf) are encountered. -export interface DebuggerNumericsAlertReport { - device_name: string; - tensor_name: string; - first_timestamp: number; - nan_event_count: number; - neg_inf_event_count: number; - pos_inf_event_count: number; -} -// A DebuggerNumericsAlertReportResponse contains alerts issued by the debugger -// in ascending order of timestamp. This helps the user identify for instance -// when bad values first appeared in the model. -export type DebuggerNumericsAlertReportResponse = DebuggerNumericsAlertReport[]; + // An object that encapsulates an alert issued by the debugger. This alert is + // sent by debugging libraries after bad values (NaN, +/- Inf) are encountered. + export interface DebuggerNumericsAlertReport { + device_name: string; + tensor_name: string; + first_timestamp: number; + nan_event_count: number; + neg_inf_event_count: number; + pos_inf_event_count: number; + } + // A DebuggerNumericsAlertReportResponse contains alerts issued by the debugger + // in ascending order of timestamp. This helps the user identify for instance + // when bad values first appeared in the model. + export type DebuggerNumericsAlertReportResponse = DebuggerNumericsAlertReport[]; -export const TYPES = []; + export const TYPES = []; -/** Given a RunToTag, return sorted array of all runs */ -export function getRunsNamed(r: RunToTag): string[] { - return _.keys(r).sort(vz_sorting.compareTagNames); -} + /** Given a RunToTag, return sorted array of all runs */ + export function getRunsNamed(r: RunToTag): string[] { + return _.keys(r).sort(vz_sorting.compareTagNames); + } -/** Given a RunToTag, return array of all tags (sorted + dedup'd) */ -export function getTags(r: RunToTag): string[] { - return _.union.apply(null, _.values(r)).sort(vz_sorting.compareTagNames); -} + /** Given a RunToTag, return array of all tags (sorted + dedup'd) */ + export function getTags(r: RunToTag): string[] { + return _.union.apply(null, _.values(r)).sort(vz_sorting.compareTagNames); + } -/** - * Given a RunToTag and an array of runs, return every tag that appears for - * at least one run. - * Sorted, deduplicated. - */ -export function filterTags(r: RunToTag, runs: string[]): string[] { - let result = []; - runs.forEach((x) => result = result.concat(r[x])); - return _.uniq(result).sort(vz_sorting.compareTagNames); -} + /** + * Given a RunToTag and an array of runs, return every tag that appears for + * at least one run. + * Sorted, deduplicated. + */ + export function filterTags(r: RunToTag, runs: string[]): string[] { + let result = []; + runs.forEach((x) => (result = result.concat(r[x]))); + return _.uniq(result).sort(vz_sorting.compareTagNames); + } -function timeToDate(x: number): Date { - return new Date(x * 1000); -}; + function timeToDate(x: number): Date { + return new Date(x * 1000); + } -/** Just a curryable map to make things cute and tidy. */ -function map(f: (x: T) => U): (arr: T[]) => U[] { - return function(arr: T[]): U[] { - return arr.map(f); - }; -}; - -/** - * This is a higher order function that takes a function that transforms a - * T into a G, and returns a function that takes TupleDatas and converts - * them into the intersection of a G and a Datum. - */ -function detupler(xform: (x: T) => G): (t: TupleData) => Datum & G { - return function(x: TupleData): Datum & G { - // Create a G, assert it has type - let obj = xform(x[2]); - // ... patch in the properties of datum - obj.wall_time = timeToDate(x[0]); - obj.step = x[1]; - return obj; - }; -}; + /** Just a curryable map to make things cute and tidy. */ + function map(f: (x: T) => U): (arr: T[]) => U[] { + return function(arr: T[]): U[] { + return arr.map(f); + }; + } -/** - * The following interface (TupleData) describes how the data is sent - * over from the backend. - */ -type TupleData = [number, number, T]; // wall_time, step + /** + * This is a higher order function that takes a function that transforms a + * T into a G, and returns a function that takes TupleDatas and converts + * them into the intersection of a G and a Datum. + */ + function detupler(xform: (x: T) => G): (t: TupleData) => Datum & G { + return function(x: TupleData): Datum & G { + // Create a G, assert it has type + let obj = xform(x[2]); + // ... patch in the properties of datum + obj.wall_time = timeToDate(x[0]); + obj.step = x[1]; + return obj; + }; + } -} // namespace tf_backend + /** + * The following interface (TupleData) describes how the data is sent + * over from the backend. + */ + type TupleData = [number, number, T]; // wall_time, step +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/baseStore.ts b/tensorboard/components/tf_backend/baseStore.ts index 3d3652f07a..c3fefac7c4 100644 --- a/tensorboard/components/tf_backend/baseStore.ts +++ b/tensorboard/components/tf_backend/baseStore.ts @@ -13,65 +13,63 @@ +limitations under the License. +==============================================================================*/ namespace tf_backend { + export type Listener = () => void; -export type Listener = () => void; - -// A unique reference to a listener for an easier dereferencing. -export class ListenKey { - public readonly listener: Listener; - constructor(listener: Listener) { - this.listener = listener; + // A unique reference to a listener for an easier dereferencing. + export class ListenKey { + public readonly listener: Listener; + constructor(listener: Listener) { + this.listener = listener; + } } -} -export abstract class BaseStore { - protected requestManager: RequestManager = - new RequestManager(1 /* simultaneous request */); - private _listeners: Set = new Set(); - public initialized: boolean = false; + export abstract class BaseStore { + protected requestManager: RequestManager = new RequestManager( + 1 /* simultaneous request */ + ); + private _listeners: Set = new Set(); + public initialized: boolean = false; - /** - * Asynchronously load or reload the runs data. Listeners will be - * invoked if this causes the runs data to change. - * - * @see addListener - * @return {Promise} a promise that resolves when new data have loaded. - */ - protected abstract load(): Promise; + /** + * Asynchronously load or reload the runs data. Listeners will be + * invoked if this causes the runs data to change. + * + * @see addListener + * @return {Promise} a promise that resolves when new data have loaded. + */ + protected abstract load(): Promise; - refresh(): Promise { - return this.load().then(() => { - this.initialized = true; - }); - } + refresh(): Promise { + return this.load().then(() => { + this.initialized = true; + }); + } - /** - * Register a listener (nullary function) to be called when new runs are - * available. - */ - addListener(listener: Listener): ListenKey { - const key = new ListenKey(listener); - this._listeners.add(key); - return key; - } + /** + * Register a listener (nullary function) to be called when new runs are + * available. + */ + addListener(listener: Listener): ListenKey { + const key = new ListenKey(listener); + this._listeners.add(key); + return key; + } - /** - * Remove a listener registered with `addListener`. - */ - removeListenerByKey(listenKey: ListenKey): void { - this._listeners.delete(listenKey); - } + /** + * Remove a listener registered with `addListener`. + */ + removeListenerByKey(listenKey: ListenKey): void { + this._listeners.delete(listenKey); + } - protected emitChange(): void { - this._listeners.forEach(listenKey => { - try { - listenKey.listener(); - } catch (e) { - // ignore exceptions on the listener side. - } - }); + protected emitChange(): void { + this._listeners.forEach((listenKey) => { + try { + listenKey.listener(); + } catch (e) { + // ignore exceptions on the listener side. + } + }); + } } - -} - } // namespace tf_backend diff --git a/tensorboard/components/tf_backend/canceller.ts b/tensorboard/components/tf_backend/canceller.ts index 58571ad9dc..be389934bf 100644 --- a/tensorboard/components/tf_backend/canceller.ts +++ b/tensorboard/components/tf_backend/canceller.ts @@ -13,56 +13,55 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { - -/** - * A class that allows marking promises as cancelled. - * - * This can be useful to, e.g., prevent old network requests from - * stomping new ones and writing bad data. - * - * Usage: - * - * const canceller = new Canceller(); - * let myPromise: Promise = getPromise(); - * myPromise.then(canceller.cancellable(({value, cancelled} => { - * if (cancelled) { - * console.warn("Don't make promises you can't keep >:-{"); - * } - * console.log("Enjoy your value:", value); - * })); - * - * // If `myPromise` is resolved now, then `cancelled` will be `false`. - * canceller.cancelAll(); - * // If `myPromise` is resolved now, then `cancelled` will be `true`. - */ -export class Canceller { /** - * How many times has `cancelAll` been called? + * A class that allows marking promises as cancelled. + * + * This can be useful to, e.g., prevent old network requests from + * stomping new ones and writing bad data. + * + * Usage: + * + * const canceller = new Canceller(); + * let myPromise: Promise = getPromise(); + * myPromise.then(canceller.cancellable(({value, cancelled} => { + * if (cancelled) { + * console.warn("Don't make promises you can't keep >:-{"); + * } + * console.log("Enjoy your value:", value); + * })); + * + * // If `myPromise` is resolved now, then `cancelled` will be `false`. + * canceller.cancelAll(); + * // If `myPromise` is resolved now, then `cancelled` will be `true`. */ - private cancellationCount = 0; + export class Canceller { + /** + * How many times has `cancelAll` been called? + */ + private cancellationCount = 0; - /** - * Create a cancellable task. This returns a new function that, when - * invoked, will pass its argument to the provided function as well as - * a `cancelled` argument. This argument will be `false` unless and - * until `cancelAll` is invoked after the creation of this task. - */ - public cancellable(f: (result: {value: T, cancelled: boolean}) => U): - (T) => U { - const originalCancellationCount = this.cancellationCount; - return (value) => { - const cancelled = this.cancellationCount !== originalCancellationCount; - return f({value, cancelled}); - }; - } + /** + * Create a cancellable task. This returns a new function that, when + * invoked, will pass its argument to the provided function as well as + * a `cancelled` argument. This argument will be `false` unless and + * until `cancelAll` is invoked after the creation of this task. + */ + public cancellable( + f: (result: {value: T; cancelled: boolean}) => U + ): (T) => U { + const originalCancellationCount = this.cancellationCount; + return (value) => { + const cancelled = this.cancellationCount !== originalCancellationCount; + return f({value, cancelled}); + }; + } - /** - * Mark all outstanding tasks as cancelled. Tasks not yet created will - * not be affected. - */ - public cancelAll(): void { - this.cancellationCount++; + /** + * Mark all outstanding tasks as cancelled. Tasks not yet created will + * not be affected. + */ + public cancelAll(): void { + this.cancellationCount++; + } } -} - -} // namespace tf_backend +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/environmentStore.ts b/tensorboard/components/tf_backend/environmentStore.ts index 6798df5772..5c44c60567 100644 --- a/tensorboard/components/tf_backend/environmentStore.ts +++ b/tensorboard/components/tf_backend/environmentStore.ts @@ -13,49 +13,47 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { - -export enum Mode { - DB, - LOGDIR, -} - -interface Environment { - dataLocation: string, - mode: Mode, - windowTitle: string, -} - -export class EnvironmentStore extends BaseStore { - private environment: Environment; - - load() { - const url = tf_backend.getRouter().environment(); - return this.requestManager.request(url).then(result => { - const environment = { - dataLocation: result.data_location, - mode: result.mode == 'db' ? Mode.DB : Mode.LOGDIR, - windowTitle: result.window_title, - }; - if (_.isEqual(this.environment, environment)) return; - - this.environment = environment; - this.emitChange(); - }); - } - - public getDataLocation(): string { - return this.environment ? this.environment.dataLocation : ''; + export enum Mode { + DB, + LOGDIR, } - public getMode(): Mode { - return this.environment ? this.environment.mode : null; + interface Environment { + dataLocation: string; + mode: Mode; + windowTitle: string; } - public getWindowTitle(): string { - return this.environment ? this.environment.windowTitle : ''; + export class EnvironmentStore extends BaseStore { + private environment: Environment; + + load() { + const url = tf_backend.getRouter().environment(); + return this.requestManager.request(url).then((result) => { + const environment = { + dataLocation: result.data_location, + mode: result.mode == 'db' ? Mode.DB : Mode.LOGDIR, + windowTitle: result.window_title, + }; + if (_.isEqual(this.environment, environment)) return; + + this.environment = environment; + this.emitChange(); + }); + } + + public getDataLocation(): string { + return this.environment ? this.environment.dataLocation : ''; + } + + public getMode(): Mode { + return this.environment ? this.environment.mode : null; + } + + public getWindowTitle(): string { + return this.environment ? this.environment.windowTitle : ''; + } } -} - -export const environmentStore = new EnvironmentStore(); -} // namespace tf_backend + export const environmentStore = new EnvironmentStore(); +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/experimentsStore.ts b/tensorboard/components/tf_backend/experimentsStore.ts index e6aed45b05..5f0ab0c193 100644 --- a/tensorboard/components/tf_backend/experimentsStore.ts +++ b/tensorboard/components/tf_backend/experimentsStore.ts @@ -13,30 +13,28 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { - -export class ExperimentsStore extends BaseStore { - private _experiments: Experiment[] = []; - - load() { - const url = getRouter().experiments(); - return this.requestManager.request(url).then(newExperiments => { - if (!_.isEqual(this._experiments, newExperiments)) { - this._experiments = newExperiments; - this.emitChange(); - } - }); + export class ExperimentsStore extends BaseStore { + private _experiments: Experiment[] = []; + + load() { + const url = getRouter().experiments(); + return this.requestManager.request(url).then((newExperiments) => { + if (!_.isEqual(this._experiments, newExperiments)) { + this._experiments = newExperiments; + this.emitChange(); + } + }); + } + + /** + * Get the current list of experiments. If no data is available, this will be + * an empty array (i.e., there is no distinction between "no experiment" and + * "no experiment yet"). + */ + getExperiments(): Experiment[] { + return this._experiments.slice(); + } } - /** - * Get the current list of experiments. If no data is available, this will be - * an empty array (i.e., there is no distinction between "no experiment" and - * "no experiment yet"). - */ - getExperiments(): Experiment[] { - return this._experiments.slice(); - } -} - -export const experimentsStore = new ExperimentsStore(); - -} // namespace tf_backend + export const experimentsStore = new ExperimentsStore(); +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/requestManager.ts b/tensorboard/components/tf_backend/requestManager.ts index 338c5f9154..f50f7359c2 100644 --- a/tensorboard/components/tf_backend/requestManager.ts +++ b/tensorboard/components/tf_backend/requestManager.ts @@ -13,287 +13,302 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { + interface ResolveReject { + resolve: Function; + reject: Function; + } + /** + * Manages many fetch requests. Launches up to nSimultaneousRequests + * simultaneously, and maintains a LIFO queue of requests to process when + * more urls are requested than can be handled at once. The queue can be + * cleared. + * + * When a request is made, a Promise is returned which resolves with the + * parsed JSON result from the request. + */ + export class RequestCancellationError extends Error { + public name = 'RequestCancellationError'; + } -interface ResolveReject { - resolve: Function; - reject: Function; -} - -/** - * Manages many fetch requests. Launches up to nSimultaneousRequests - * simultaneously, and maintains a LIFO queue of requests to process when - * more urls are requested than can be handled at once. The queue can be - * cleared. - * - * When a request is made, a Promise is returned which resolves with the - * parsed JSON result from the request. - */ -export class RequestCancellationError extends Error { - public name = 'RequestCancellationError'; -} - -export class InvalidRequestOptionsError extends Error { - public name = 'InvalidRequestOptionsError'; - constructor(msg : string) { - super(msg); - // The following is needed due to a limitation of TypeScript when - // extending 'Error'. See: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - Object.setPrototypeOf(this, InvalidRequestOptionsError.prototype); + export class InvalidRequestOptionsError extends Error { + public name = 'InvalidRequestOptionsError'; + constructor(msg: string) { + super(msg); + // The following is needed due to a limitation of TypeScript when + // extending 'Error'. See: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, InvalidRequestOptionsError.prototype); + } } -} -export class RequestNetworkError extends Error { - public name: string; - public req: XMLHttpRequest; - public url: string; + export class RequestNetworkError extends Error { + public name: string; + public req: XMLHttpRequest; + public url: string; - constructor(req: XMLHttpRequest, url) { - super(); - this.message = `RequestNetworkError: ${req.status} at ${url}`; - this.name = 'RequestNetworkError'; - this.req = req; - this.url = url; + constructor(req: XMLHttpRequest, url) { + super(); + this.message = `RequestNetworkError: ${req.status} at ${url}`; + this.name = 'RequestNetworkError'; + this.req = req; + this.url = url; + } } -} -/** The HTTP method-type to use. Currently only 'GET' and 'POST' are - * supported. - */ -export enum HttpMethodType { - GET = 'GET', - POST = 'POST', -} + /** The HTTP method-type to use. Currently only 'GET' and 'POST' are + * supported. + */ + export enum HttpMethodType { + GET = 'GET', + POST = 'POST', + } -/** - * Holds options that can be used to configure the HTTP request. - */ -export class RequestOptions { - public methodType: HttpMethodType; + /** + * Holds options that can be used to configure the HTTP request. + */ + export class RequestOptions { + public methodType: HttpMethodType; - /** The content-type request header to use. Cannot be set for a GET request.*/ - public contentType?: string; + /** The content-type request header to use. Cannot be set for a GET request.*/ + public contentType?: string; - /** The request body to use. This is the object that is passed to the - * XMLHttpRequest.send() method. If not given the 'send' method is called - * without an argument. - */ - public body?: any; + /** The request body to use. This is the object that is passed to the + * XMLHttpRequest.send() method. If not given the 'send' method is called + * without an argument. + */ + public body?: any; - /** If specified, this will be the value set in the - * XMLHttpRequest.withCredentials property. - */ - public withCredentials?: boolean; + /** If specified, this will be the value set in the + * XMLHttpRequest.withCredentials property. + */ + public withCredentials?: boolean; - // Validates this object. Throws InvalidRequestOptionsError on error. - public validate() { - if (this.methodType === HttpMethodType.GET) { - // We don't allow a body for a GET. - if (this.body) { - throw new InvalidRequestOptionsError( - 'body must be missing for a GET request.'); + // Validates this object. Throws InvalidRequestOptionsError on error. + public validate() { + if (this.methodType === HttpMethodType.GET) { + // We don't allow a body for a GET. + if (this.body) { + throw new InvalidRequestOptionsError( + 'body must be missing for a GET request.' + ); + } } + // We allow body-less or contentType-less POSTs even if they don't + // make much sense. } - // We allow body-less or contentType-less POSTs even if they don't - // make much sense. } -} -export class RequestManager { - private _queue: ResolveReject[]; - private _maxRetries: number; - private _nActiveRequests: number; - private _nSimultaneousRequests: number; + export class RequestManager { + private _queue: ResolveReject[]; + private _maxRetries: number; + private _nActiveRequests: number; + private _nSimultaneousRequests: number; - constructor(nSimultaneousRequests = 10, maxRetries = 3) { - this._queue = []; - this._nActiveRequests = 0; - this._nSimultaneousRequests = nSimultaneousRequests; - this._maxRetries = maxRetries; - } + constructor(nSimultaneousRequests = 10, maxRetries = 3) { + this._queue = []; + this._nActiveRequests = 0; + this._nSimultaneousRequests = nSimultaneousRequests; + this._maxRetries = maxRetries; + } - /** - * Gives a promise that loads assets from given url (respects queuing). If - * postData is provided, this request will use POST, not GET. This is an - * object mapping POST keys to string values. - */ - public request(url: string, postData?: {[key: string]: string}): - Promise { - const requestOptions = requestOptionsFromPostData(postData); - return this.requestWithOptions(url, requestOptions); - } + /** + * Gives a promise that loads assets from given url (respects queuing). If + * postData is provided, this request will use POST, not GET. This is an + * object mapping POST keys to string values. + */ + public request( + url: string, + postData?: {[key: string]: string} + ): Promise { + const requestOptions = requestOptionsFromPostData(postData); + return this.requestWithOptions(url, requestOptions); + } - public requestWithOptions(url: string, requestOptions: RequestOptions): - Promise { - requestOptions.validate(); - const promise = new Promise((resolve, reject) => { + public requestWithOptions( + url: string, + requestOptions: RequestOptions + ): Promise { + requestOptions.validate(); + const promise = new Promise((resolve, reject) => { const resolver = {resolve: resolve, reject: reject}; this._queue.push(resolver); this.launchRequests(); }) - .then(() => { - return this.promiseWithRetries(url, this._maxRetries, requestOptions); - }) - .then( - (response) => { - // Success - Let's free space for another active - // request, and launch it - this._nActiveRequests--; - this.launchRequests(); - return response; - }, - (rejection) => { - if (rejection.name === 'RequestNetworkError') { - // If we failed due to network error, we should - // decrement - // _nActiveRequests because this request was - // active + .then(() => { + return this.promiseWithRetries(url, this._maxRetries, requestOptions); + }) + .then( + (response) => { + // Success - Let's free space for another active + // request, and launch it this._nActiveRequests--; this.launchRequests(); + return response; + }, + (rejection) => { + if (rejection.name === 'RequestNetworkError') { + // If we failed due to network error, we should + // decrement + // _nActiveRequests because this request was + // active + this._nActiveRequests--; + this.launchRequests(); + } + return Promise.reject(rejection); } - return Promise.reject(rejection); - }); - return promise; - } + ); + return promise; + } - public fetch(url: string, fetchOptions?: RequestInit): Promise { - return new Promise((resolve, reject) => { + public fetch(url: string, fetchOptions?: RequestInit): Promise { + return new Promise((resolve, reject) => { const resolver = {resolve: resolve, reject: reject}; this._queue.push(resolver); this.launchRequests(); - }).then(() => { - let numTries = 1; - return new Promise((resolve) => { - const retryFetch = () => { - fetch(url, fetchOptions).then((response) => { - if (!response.ok && this._maxRetries > numTries) { - numTries++; - retryFetch(); - return; - } - resolve(response); - this._nActiveRequests--; - this.launchRequests(); - }); - } + }).then(() => { + let numTries = 1; + return new Promise((resolve) => { + const retryFetch = () => { + fetch(url, fetchOptions).then((response) => { + if (!response.ok && this._maxRetries > numTries) { + numTries++; + retryFetch(); + return; + } + resolve(response); + this._nActiveRequests--; + this.launchRequests(); + }); + }; - retryFetch(); + retryFetch(); + }); }); - }); - } - - public clearQueue() { - while (this._queue.length > 0) { - this._queue.pop().reject( - new RequestCancellationError('Request cancelled by clearQueue')); } - } - /* Return number of currently pending requests */ - public activeRequests(): number { - return this._nActiveRequests; - } + public clearQueue() { + while (this._queue.length > 0) { + this._queue + .pop() + .reject( + new RequestCancellationError('Request cancelled by clearQueue') + ); + } + } - /* Return total number of outstanding requests (includes queue) */ - public outstandingRequests(): number { - return this._nActiveRequests + this._queue.length; - } + /* Return number of currently pending requests */ + public activeRequests(): number { + return this._nActiveRequests; + } - private launchRequests() { - while (this._nActiveRequests < this._nSimultaneousRequests && - this._queue.length > 0) { - this._nActiveRequests++; - this._queue.pop().resolve(); + /* Return total number of outstanding requests (includes queue) */ + public outstandingRequests(): number { + return this._nActiveRequests + this._queue.length; } - } - /** - * Try to request a given URL using overwritable _promiseFromUrl method. - * If the request fails for any reason, we will retry up to maxRetries - * times. In practice, this will help us paper over transient network issues - * like '502 Bad Gateway'. - * By default, Chrome displays network errors in console, so - * the user will be able to tell when the requests are failing. I think this - * is a feature, if the request failures and retries are causing any - * pain to users, they can see it and file issues. - */ - private promiseWithRetries( - url: string, maxRetries: number, requestOptions: RequestOptions) { - var success = (x) => x; - var failure = (x) => { - if (maxRetries > 0) { - return this.promiseWithRetries(url, maxRetries - 1, requestOptions); - } else { - return Promise.reject(x); + private launchRequests() { + while ( + this._nActiveRequests < this._nSimultaneousRequests && + this._queue.length > 0 + ) { + this._nActiveRequests++; + this._queue.pop().resolve(); } - }; - return this._promiseFromUrl(url, requestOptions).then(success, failure); - } + } - /* Actually get promise from url using XMLHttpRequest */ - protected _promiseFromUrl(url: string, requestOptions: RequestOptions) { - return new Promise((resolve, reject) => { - const req = buildXMLHttpRequest( - requestOptions.methodType, - url, - requestOptions.withCredentials, - requestOptions.contentType); - req.onload = function() { - if (req.status === 200) { - resolve(JSON.parse(req.responseText)); + /** + * Try to request a given URL using overwritable _promiseFromUrl method. + * If the request fails for any reason, we will retry up to maxRetries + * times. In practice, this will help us paper over transient network issues + * like '502 Bad Gateway'. + * By default, Chrome displays network errors in console, so + * the user will be able to tell when the requests are failing. I think this + * is a feature, if the request failures and retries are causing any + * pain to users, they can see it and file issues. + */ + private promiseWithRetries( + url: string, + maxRetries: number, + requestOptions: RequestOptions + ) { + var success = (x) => x; + var failure = (x) => { + if (maxRetries > 0) { + return this.promiseWithRetries(url, maxRetries - 1, requestOptions); } else { - reject(new RequestNetworkError(req, url)); + return Promise.reject(x); } }; - req.onerror = function() { - reject(new RequestNetworkError(req, url)); - }; - if (requestOptions.body) { - req.send(requestOptions.body); - } - else { - req.send(); - } - }); - } -} + return this._promiseFromUrl(url, requestOptions).then(success, failure); + } -function buildXMLHttpRequest(methodType: HttpMethodType, url: string, - withCredentials?: boolean, - contentType?: string): XMLHttpRequest { - const req = new XMLHttpRequest(); - req.open(methodType, url); - if (withCredentials) { - req.withCredentials = withCredentials; + /* Actually get promise from url using XMLHttpRequest */ + protected _promiseFromUrl(url: string, requestOptions: RequestOptions) { + return new Promise((resolve, reject) => { + const req = buildXMLHttpRequest( + requestOptions.methodType, + url, + requestOptions.withCredentials, + requestOptions.contentType + ); + req.onload = function() { + if (req.status === 200) { + resolve(JSON.parse(req.responseText)); + } else { + reject(new RequestNetworkError(req, url)); + } + }; + req.onerror = function() { + reject(new RequestNetworkError(req, url)); + }; + if (requestOptions.body) { + req.send(requestOptions.body); + } else { + req.send(); + } + }); + } } - if (contentType) { - req.setRequestHeader('Content-Type', contentType); + + function buildXMLHttpRequest( + methodType: HttpMethodType, + url: string, + withCredentials?: boolean, + contentType?: string + ): XMLHttpRequest { + const req = new XMLHttpRequest(); + req.open(methodType, url); + if (withCredentials) { + req.withCredentials = withCredentials; + } + if (contentType) { + req.setRequestHeader('Content-Type', contentType); + } + return req; } - return req; -} -function requestOptionsFromPostData(postData?: {[key: string]: string}): - RequestOptions { - const result = new RequestOptions(); - if (!postData) { - result.methodType = HttpMethodType.GET; + function requestOptionsFromPostData(postData?: { + [key: string]: string; + }): RequestOptions { + const result = new RequestOptions(); + if (!postData) { + result.methodType = HttpMethodType.GET; + return result; + } + result.methodType = HttpMethodType.POST; + result.body = formDataFromDictionary(postData); return result; } - result.methodType = HttpMethodType.POST; - result.body = formDataFromDictionary(postData); - return result; -} -function formDataFromDictionary(postData: {[key: string]: string}) { - const formData = new FormData(); - for (let postKey in postData) { - if (postKey) { - // The linter requires 'for in' loops to be filtered by an if - // condition. - formData.append(postKey, postData[postKey]); + function formDataFromDictionary(postData: {[key: string]: string}) { + const formData = new FormData(); + for (let postKey in postData) { + if (postKey) { + // The linter requires 'for in' loops to be filtered by an if + // condition. + formData.append(postKey, postData[postKey]); + } } + return formData; } - return formData; -} - -} // namespace tf_backend +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/router.ts b/tensorboard/components/tf_backend/router.ts index 86a78a79cf..e9ba029ca9 100644 --- a/tensorboard/components/tf_backend/router.ts +++ b/tensorboard/components/tf_backend/router.ts @@ -13,102 +13,102 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { - -export interface Router { - environment: () => string; - experiments: () => string; - pluginRoute: ( - pluginName: string, - route: string, - params?: URLSearchParams - ) => string; - pluginsListing: () => string; - runs: () => string; - runsForExperiment: (id: tf_backend.ExperimentId) => string; -}; - -let _router: Router = createRouter(); - -/** - * Create a router for communicating with the TensorBoard backend. You - * can pass this to `setRouter` to make it the global router. - * - * @param dataDir {string=} The base prefix for data endpoints. - */ -export function createRouter(dataDir = "data"): Router { - if (dataDir[dataDir.length - 1] === "/") { - dataDir = dataDir.slice(0, dataDir.length - 1); - } - return { - environment: () => createDataPath(dataDir, "/environment"), - experiments: () => createDataPath(dataDir, "/experiments"), + export interface Router { + environment: () => string; + experiments: () => string; pluginRoute: ( pluginName: string, route: string, params?: URLSearchParams - ): string => { - return createDataPath( - dataDir + "/plugin", - `/${pluginName}${route}`, - params - ); - }, - pluginsListing: () => createDataPath(dataDir, "/plugins_listing"), - runs: () => createDataPath(dataDir, "/runs"), - runsForExperiment: (id) => { - return createDataPath( - dataDir, - "/experiment_runs", - createSearchParam({experiment: String(id)}) - ); - }, - }; -} + ) => string; + pluginsListing: () => string; + runs: () => string; + runsForExperiment: (id: tf_backend.ExperimentId) => string; + } -/** - * @return {Router} the global router - */ -export function getRouter(): Router { - return _router; -} + let _router: Router = createRouter(); -/** - * Set the global router, to be returned by future calls to `getRouter`. - * You may wish to invoke this if you are running a demo server with a - * custom path prefix, or if you have customized the TensorBoard backend - * to use a different path. - * - * @param {Router} router the new global router - */ -export function setRouter(router: Router): void { - if (router == null) { - throw new Error('Router required, but got: ' + router); + /** + * Create a router for communicating with the TensorBoard backend. You + * can pass this to `setRouter` to make it the global router. + * + * @param dataDir {string=} The base prefix for data endpoints. + */ + export function createRouter(dataDir = 'data'): Router { + if (dataDir[dataDir.length - 1] === '/') { + dataDir = dataDir.slice(0, dataDir.length - 1); + } + return { + environment: () => createDataPath(dataDir, '/environment'), + experiments: () => createDataPath(dataDir, '/experiments'), + pluginRoute: ( + pluginName: string, + route: string, + params?: URLSearchParams + ): string => { + return createDataPath( + dataDir + '/plugin', + `/${pluginName}${route}`, + params + ); + }, + pluginsListing: () => createDataPath(dataDir, '/plugins_listing'), + runs: () => createDataPath(dataDir, '/runs'), + runsForExperiment: (id) => { + return createDataPath( + dataDir, + '/experiment_runs', + createSearchParam({experiment: String(id)}) + ); + }, + }; } - _router = router; -} -function createDataPath( - dataDir: string, - route: string, - params: URLSearchParams = new URLSearchParams() -): string { - let relativePath = dataDir + route; - if (String(params)) { - const delimiter = route.includes("?") ? "&" : "?"; - relativePath += delimiter + String(params); + /** + * @return {Router} the global router + */ + export function getRouter(): Router { + return _router; } - return relativePath; -} -export function createSearchParam(params: QueryParams = {}): URLSearchParams { - const keys = Object.keys(params).sort().filter(k => params[k]); - const searchParams = new URLSearchParams(); - keys.forEach(key => { - const values = params[key]; - const array = Array.isArray(values) ? values : [values]; - array.forEach(val => searchParams.append(key, val)); - }); - return searchParams; -} + /** + * Set the global router, to be returned by future calls to `getRouter`. + * You may wish to invoke this if you are running a demo server with a + * custom path prefix, or if you have customized the TensorBoard backend + * to use a different path. + * + * @param {Router} router the new global router + */ + export function setRouter(router: Router): void { + if (router == null) { + throw new Error('Router required, but got: ' + router); + } + _router = router; + } -} // namespace tf_backend + function createDataPath( + dataDir: string, + route: string, + params: URLSearchParams = new URLSearchParams() + ): string { + let relativePath = dataDir + route; + if (String(params)) { + const delimiter = route.includes('?') ? '&' : '?'; + relativePath += delimiter + String(params); + } + return relativePath; + } + + export function createSearchParam(params: QueryParams = {}): URLSearchParams { + const keys = Object.keys(params) + .sort() + .filter((k) => params[k]); + const searchParams = new URLSearchParams(); + keys.forEach((key) => { + const values = params[key]; + const array = Array.isArray(values) ? values : [values]; + array.forEach((val) => searchParams.append(key, val)); + }); + return searchParams; + } +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/runsStore.ts b/tensorboard/components/tf_backend/runsStore.ts index 235c0ab886..b8c870ccb7 100644 --- a/tensorboard/components/tf_backend/runsStore.ts +++ b/tensorboard/components/tf_backend/runsStore.ts @@ -13,30 +13,28 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { - -export class RunsStore extends BaseStore { - private _runs: string[] = []; - - load() { - const url = getRouter().runs(); - return this.requestManager.request(url).then(newRuns => { - if (!_.isEqual(this._runs, newRuns)) { - this._runs = newRuns; - this.emitChange(); - } - }); + export class RunsStore extends BaseStore { + private _runs: string[] = []; + + load() { + const url = getRouter().runs(); + return this.requestManager.request(url).then((newRuns) => { + if (!_.isEqual(this._runs, newRuns)) { + this._runs = newRuns; + this.emitChange(); + } + }); + } + + /** + * Get the current list of runs. If no data is available, this will be + * an empty array (i.e., there is no distinction between "no runs" and + * "no runs yet"). + */ + getRuns(): string[] { + return this._runs.slice(); + } } - /** - * Get the current list of runs. If no data is available, this will be - * an empty array (i.e., there is no distinction between "no runs" and - * "no runs yet"). - */ - getRuns(): string[] { - return this._runs.slice(); - } -} - -export const runsStore = new RunsStore(); - -} // namespace tf_backend + export const runsStore = new RunsStore(); +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/test/backendTests.ts b/tensorboard/components/tf_backend/test/backendTests.ts index 93a184785d..4225e37540 100644 --- a/tensorboard/components/tf_backend/test/backendTests.ts +++ b/tensorboard/components/tf_backend/test/backendTests.ts @@ -13,160 +13,180 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { + const {assert} = chai; -const {assert} = chai; - -describe('urlPathHelpers', () => { - it('addParams leaves input untouched when there are no parameters', () => { - const actual = addParams('http://foo', {a: undefined, b: undefined}); - const expected = 'http://foo'; - chai.assert.equal(actual, expected); - }); - it('addParams adds parameters to a URL without parameters', () => { - const actual = addParams( - 'http://foo', - {a: "1", b: ["2", "3+4"], c: "5", d: undefined}); - const expected = 'http://foo?a=1&b=2&b=3%2B4&c=5'; - chai.assert.equal(actual, expected); - }); - it('addParams adds parameters to a URL with parameters', () => { - const actual = addParams( - 'http://foo?a=1', - {b: ["2", "3+4"], c: "5", d: undefined}); - const expected = 'http://foo?a=1&b=2&b=3%2B4&c=5'; - chai.assert.equal(actual, expected); - }); -}); - -function assertIsDatum(x) { - chai.assert.isNumber(x.step); - chai.assert.instanceOf(x.wall_time, Date); -} - -describe('backend tests', () => { - it('runToTag helpers work', () => { - const r2t: RunToTag = { - run1: ['foo', 'bar', 'zod'], - run2: ['zod', 'zoink'], - a: ['foo', 'zod'] - }; - const empty1: RunToTag = {}; - const empty2: RunToTag = {run1: [], run2: []}; - chai.assert.deepEqual(getRunsNamed(r2t), ['a', 'run1', 'run2']); - chai.assert.deepEqual(getTags(r2t), ['bar', 'foo', 'zod', 'zoink']); - chai.assert.deepEqual(filterTags(r2t, ['run1', 'run2']), getTags(r2t)); - chai.assert.deepEqual(filterTags(r2t, ['run1']), ['bar', 'foo', 'zod']); - chai.assert.deepEqual( - filterTags(r2t, ['run2', 'a']), ['foo', 'zod', 'zoink']); - - chai.assert.deepEqual(getRunsNamed(empty1), []); - chai.assert.deepEqual(getTags(empty1), []); - - chai.assert.deepEqual(getRunsNamed(empty2), ['run1', 'run2']); - chai.assert.deepEqual(getTags(empty2), []); - }); - - describe('router', () => { - describe('prod mode', () => { - let router: Router; - beforeEach(() => { - router = createRouter('data'); - }); - - it('leading slash in pathPrefix is an absolute path', () => { - const router = createRouter('/data/'); - assert.equal(router.runs(), '/data/runs'); - }); - - it('returns complete pathname when pathPrefix omits slash', () => { - const router = createRouter('data/'); - assert.equal(router.runs(), 'data/runs'); + describe('urlPathHelpers', () => { + it('addParams leaves input untouched when there are no parameters', () => { + const actual = addParams('http://foo', {a: undefined, b: undefined}); + const expected = 'http://foo'; + chai.assert.equal(actual, expected); + }); + it('addParams adds parameters to a URL without parameters', () => { + const actual = addParams('http://foo', { + a: '1', + b: ['2', '3+4'], + c: '5', + d: undefined, }); - - it('does not prune many leading slashes that forms full url', () => { - const router = createRouter('///data/hello'); - // This becomes 'http://data/hello/runs' - assert.equal(router.runs(), '///data/hello/runs'); + const expected = 'http://foo?a=1&b=2&b=3%2B4&c=5'; + chai.assert.equal(actual, expected); + }); + it('addParams adds parameters to a URL with parameters', () => { + const actual = addParams('http://foo?a=1', { + b: ['2', '3+4'], + c: '5', + d: undefined, }); + const expected = 'http://foo?a=1&b=2&b=3%2B4&c=5'; + chai.assert.equal(actual, expected); + }); + }); - it('returns correct value for #environment', () => { - assert.equal(router.environment(), 'data/environment'); - }); + function assertIsDatum(x) { + chai.assert.isNumber(x.step); + chai.assert.instanceOf(x.wall_time, Date); + } + + describe('backend tests', () => { + it('runToTag helpers work', () => { + const r2t: RunToTag = { + run1: ['foo', 'bar', 'zod'], + run2: ['zod', 'zoink'], + a: ['foo', 'zod'], + }; + const empty1: RunToTag = {}; + const empty2: RunToTag = {run1: [], run2: []}; + chai.assert.deepEqual(getRunsNamed(r2t), ['a', 'run1', 'run2']); + chai.assert.deepEqual(getTags(r2t), ['bar', 'foo', 'zod', 'zoink']); + chai.assert.deepEqual(filterTags(r2t, ['run1', 'run2']), getTags(r2t)); + chai.assert.deepEqual(filterTags(r2t, ['run1']), ['bar', 'foo', 'zod']); + chai.assert.deepEqual(filterTags(r2t, ['run2', 'a']), [ + 'foo', + 'zod', + 'zoink', + ]); + + chai.assert.deepEqual(getRunsNamed(empty1), []); + chai.assert.deepEqual(getTags(empty1), []); + + chai.assert.deepEqual(getRunsNamed(empty2), ['run1', 'run2']); + chai.assert.deepEqual(getTags(empty2), []); + }); - it('returns correct value for #experiments', () => { - assert.equal(router.experiments(), 'data/experiments'); - }); + describe('router', () => { + describe('prod mode', () => { + let router: Router; + beforeEach(() => { + router = createRouter('data'); + }); - describe('#pluginRoute', () => { - it('encodes slash correctly', () => { - assert.equal( - router.pluginRoute('scalars', '/scalar'), - 'data/plugin/scalars/scalar'); + it('leading slash in pathPrefix is an absolute path', () => { + const router = createRouter('/data/'); + assert.equal(router.runs(), '/data/runs'); }); - it('encodes query param correctly', () => { - assert.equal( - router.pluginRoute( - 'scalars', - '/a', - createSearchParam({b: 'c', d: ['1', '2']})), - 'data/plugin/scalars/a?b=c&d=1&d=2'); + it('returns complete pathname when pathPrefix omits slash', () => { + const router = createRouter('data/'); + assert.equal(router.runs(), 'data/runs'); }); - it('does not put ? when passed an empty URLSearchParams', () => { - assert.equal( - router.pluginRoute('scalars', '/a', - new URLSearchParams()), - 'data/plugin/scalars/a'); + it('does not prune many leading slashes that forms full url', () => { + const router = createRouter('///data/hello'); + // This becomes 'http://data/hello/runs' + assert.equal(router.runs(), '///data/hello/runs'); }); - it('encodes parenthesis correctly', () => { - assert.equal( - router.pluginRoute('scalars', '/a', - createSearchParam({foo: '()'})), - 'data/plugin/scalars/a?foo=%28%29'); + it('returns correct value for #environment', () => { + assert.equal(router.environment(), 'data/environment'); }); - it('deals with existing query param correctly', () => { - assert.equal( - router.pluginRoute('scalars', '/a?foo=bar', - createSearchParam({hello: 'world'})), - 'data/plugin/scalars/a?foo=bar&hello=world'); + it('returns correct value for #experiments', () => { + assert.equal(router.experiments(), 'data/experiments'); }); - it('encodes query param the same as #addParams', () => { - assert.equal( + describe('#pluginRoute', () => { + it('encodes slash correctly', () => { + assert.equal( + router.pluginRoute('scalars', '/scalar'), + 'data/plugin/scalars/scalar' + ); + }); + + it('encodes query param correctly', () => { + assert.equal( router.pluginRoute( - 'scalars', - '/a', - createSearchParam({b: 'c', d: ['1']})), - addParams('data/plugin/scalars/a', {b: 'c', d: ['1']})); - assert.equal( + 'scalars', + '/a', + createSearchParam({b: 'c', d: ['1', '2']}) + ), + 'data/plugin/scalars/a?b=c&d=1&d=2' + ); + }); + + it('does not put ? when passed an empty URLSearchParams', () => { + assert.equal( + router.pluginRoute('scalars', '/a', new URLSearchParams()), + 'data/plugin/scalars/a' + ); + }); + + it('encodes parenthesis correctly', () => { + assert.equal( + router.pluginRoute( + 'scalars', + '/a', + createSearchParam({foo: '()'}) + ), + 'data/plugin/scalars/a?foo=%28%29' + ); + }); + + it('deals with existing query param correctly', () => { + assert.equal( + router.pluginRoute( + 'scalars', + '/a?foo=bar', + createSearchParam({hello: 'world'}) + ), + 'data/plugin/scalars/a?foo=bar&hello=world' + ); + }); + + it('encodes query param the same as #addParams', () => { + assert.equal( router.pluginRoute( - 'scalars', - '/a', - createSearchParam({foo: '()'})), - addParams('data/plugin/scalars/a', {foo: '()'})); + 'scalars', + '/a', + createSearchParam({b: 'c', d: ['1']}) + ), + addParams('data/plugin/scalars/a', {b: 'c', d: ['1']}) + ); + assert.equal( + router.pluginRoute( + 'scalars', + '/a', + createSearchParam({foo: '()'}) + ), + addParams('data/plugin/scalars/a', {foo: '()'}) + ); + }); }); - }); - it('returns correct value for #pluginsListing', () => { - assert.equal( - router.pluginsListing(), 'data/plugins_listing'); - }); + it('returns correct value for #pluginsListing', () => { + assert.equal(router.pluginsListing(), 'data/plugins_listing'); + }); - it('returns correct value for #runs', () => { - assert.equal(router.runs(), 'data/runs'); - }); + it('returns correct value for #runs', () => { + assert.equal(router.runs(), 'data/runs'); + }); - it('returns correct value for #runsForExperiment', () => { - assert.equal( + it('returns correct value for #runsForExperiment', () => { + assert.equal( router.runsForExperiment(1), - 'data/experiment_runs?experiment=1'); + 'data/experiment_runs?experiment=1' + ); + }); }); }); - }); -}); - -} // namespace tf_backend +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/test/requestManagerTests.ts b/tensorboard/components/tf_backend/test/requestManagerTests.ts index 2d39620829..1a74e24095 100644 --- a/tensorboard/components/tf_backend/test/requestManagerTests.ts +++ b/tensorboard/components/tf_backend/test/requestManagerTests.ts @@ -13,113 +13,113 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { + const {expect} = chai; -const {expect} = chai; - -interface MockRequest { - resolve: Function; - reject: Function; - id: number; - url: string; -} - -class MockedRequestManager extends RequestManager { - private resolvers: Function[]; - private rejectors: Function[]; - public requestsDispatched: number; - constructor(maxRequests = 10, maxRetries = 3) { - super(maxRequests, maxRetries); - this.resolvers = []; - this.rejectors = []; - this.requestsDispatched = 0; + interface MockRequest { + resolve: Function; + reject: Function; + id: number; + url: string; } - protected _promiseFromUrl(url) { - return new Promise((resolve, reject) => { - const mockJSON = { - ok: true, - json() { - return url; - }, - url, - status: 200, - }; - const mockFailedRequest: any = { - ok: false, - url, - status: 502, - }; - const mockFailure = new RequestNetworkError(mockFailedRequest, url); - this.resolvers.push(() => { - resolve(mockJSON); + + class MockedRequestManager extends RequestManager { + private resolvers: Function[]; + private rejectors: Function[]; + public requestsDispatched: number; + constructor(maxRequests = 10, maxRetries = 3) { + super(maxRequests, maxRetries); + this.resolvers = []; + this.rejectors = []; + this.requestsDispatched = 0; + } + protected _promiseFromUrl(url) { + return new Promise((resolve, reject) => { + const mockJSON = { + ok: true, + json() { + return url; + }, + url, + status: 200, + }; + const mockFailedRequest: any = { + ok: false, + url, + status: 502, + }; + const mockFailure = new RequestNetworkError(mockFailedRequest, url); + this.resolvers.push(() => { + resolve(mockJSON); + }); + this.rejectors.push(() => { + reject(mockFailure); + }); + this.requestsDispatched++; }); - this.rejectors.push(() => { - reject(mockFailure); + } + public resolveFakeRequest() { + this.resolvers.pop()(); + } + public rejectFakeRequest() { + this.rejectors.pop()(); + } + public dispatchAndResolve() { + // Wait for at least one request to be dispatched, then resolve it. + this.waitForDispatch(1).then(() => this.resolveFakeRequest()); + } + public waitForDispatch(num) { + return waitForCondition(() => { + return this.requestsDispatched >= num; }); - this.requestsDispatched++; - }); - } - public resolveFakeRequest() { - this.resolvers.pop()(); + } } - public rejectFakeRequest() { - this.rejectors.pop()(); - } - public dispatchAndResolve() { - // Wait for at least one request to be dispatched, then resolve it. - this.waitForDispatch(1).then(() => this.resolveFakeRequest()); - } - public waitForDispatch(num) { - return waitForCondition(() => { - return this.requestsDispatched >= num; + + /** Create a promise that returns when *check* returns true. + * May cause a test timeout if check never becomes true. + */ + + function waitForCondition(check: () => boolean): Promise { + return new Promise((resolve, reject) => { + const go = () => { + if (check()) { + resolve(); + } + setTimeout(go, 2); + }; + go(); }); } -} - -/** Create a promise that returns when *check* returns true. - * May cause a test timeout if check never becomes true. - */ - -function waitForCondition(check: () => boolean): Promise { - return new Promise((resolve, reject) => { - const go = () => { - if (check()) { - resolve(); - } - setTimeout(go, 2); - }; - go(); - }); -} -describe('backend', () => { - let sandbox; - beforeEach(() => { - sandbox = sinon.sandbox.create(); - }); + describe('backend', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); - afterEach(() => { - sandbox.restore(); - }); + afterEach(() => { + sandbox.restore(); + }); - describe('request manager', () => { - it('request loads JSON properly', (done) => { - const rm = new RequestManager(); - const promise = rm.request('data/example.json'); - promise.then( + describe('request manager', () => { + it('request loads JSON properly', (done) => { + const rm = new RequestManager(); + const promise = rm.request('data/example.json'); + promise.then( (response) => { chai.assert.deepEqual(response, {foo: 3, bar: 'zoidberg'}); done(); }, (reject) => { throw new Error(reject); - }); - }); + } + ); + }); - it('rejects on bad url', (done) => { - const rm = new RequestManager(5, 0); - const badUrl = '_bad_url_which_doesnt_exist.json'; - const promise = rm.request(badUrl); - promise.then( + it('rejects on bad url', (done) => { + const rm = new RequestManager(5, 0); + const badUrl = '_bad_url_which_doesnt_exist.json'; + const promise = rm.request(badUrl); + promise.then( (success) => { done(new Error('the promise should have rejected')); }, @@ -128,26 +128,27 @@ describe('backend', () => { chai.assert.include(reject.message, badUrl); chai.assert.equal(reject.req.status, 404); done(); - }); - }); + } + ); + }); - it('can retry if requests fail', (done) => { - const rm = new MockedRequestManager(3, 5); - const r = rm.request('foo'); - rm.waitForDispatch(1) + it('can retry if requests fail', (done) => { + const rm = new MockedRequestManager(3, 5); + const r = rm.request('foo'); + rm.waitForDispatch(1) .then(() => { rm.rejectFakeRequest(); return rm.waitForDispatch(2); }) .then(() => rm.resolveFakeRequest()); - r.then((success) => done()); - }); + r.then((success) => done()); + }); - it('retries at most maxRetries times', (done) => { - const MAX_RETRIES = 2; - const rm = new MockedRequestManager(3, MAX_RETRIES); - const r = rm.request('foo'); - rm.waitForDispatch(1) + it('retries at most maxRetries times', (done) => { + const MAX_RETRIES = 2; + const rm = new MockedRequestManager(3, MAX_RETRIES); + const r = rm.request('foo'); + rm.waitForDispatch(1) .then(() => { rm.rejectFakeRequest(); return rm.waitForDispatch(2); @@ -160,36 +161,55 @@ describe('backend', () => { rm.rejectFakeRequest(); }); - r.then( + r.then( (success) => done(new Error('The request should have failed')), - (failure) => done()); - }); + (failure) => done() + ); + }); - it('requestManager only sends maxRequests requests at a time', (done) => { - const rm = new MockedRequestManager(3); - const r0 = rm.request('1'); - const r1 = rm.request('2'); - const r2 = rm.request('3'); - const r3 = rm.request('4'); - chai.assert.equal(rm.activeRequests(), 3, 'three requests are active'); - chai.assert.equal( - rm.outstandingRequests(), 4, 'four requests are pending'); - rm.waitForDispatch(3) + it('requestManager only sends maxRequests requests at a time', (done) => { + const rm = new MockedRequestManager(3); + const r0 = rm.request('1'); + const r1 = rm.request('2'); + const r2 = rm.request('3'); + const r3 = rm.request('4'); + chai.assert.equal(rm.activeRequests(), 3, 'three requests are active'); + chai.assert.equal( + rm.outstandingRequests(), + 4, + 'four requests are pending' + ); + rm.waitForDispatch(3) .then(() => { chai.assert.equal( - rm.activeRequests(), 3, 'three requests are still active (1)'); + rm.activeRequests(), + 3, + 'three requests are still active (1)' + ); chai.assert.equal( - rm.requestsDispatched, 3, 'three requests were dispatched'); + rm.requestsDispatched, + 3, + 'three requests were dispatched' + ); rm.resolveFakeRequest(); return rm.waitForDispatch(4); }) .then(() => { chai.assert.equal( - rm.activeRequests(), 3, 'three requests are still active (2)'); + rm.activeRequests(), + 3, + 'three requests are still active (2)' + ); chai.assert.equal( - rm.requestsDispatched, 4, 'four requests were dispatched'); + rm.requestsDispatched, + 4, + 'four requests were dispatched' + ); chai.assert.equal( - rm.outstandingRequests(), 3, 'three requests are pending'); + rm.outstandingRequests(), + 3, + 'three requests are pending' + ); rm.resolveFakeRequest(); rm.resolveFakeRequest(); rm.resolveFakeRequest(); @@ -198,290 +218,313 @@ describe('backend', () => { .then(() => { chai.assert.equal(rm.activeRequests(), 0, 'all requests finished'); chai.assert.equal( - rm.outstandingRequests(), 0, 'no requests pending'); + rm.outstandingRequests(), + 0, + 'no requests pending' + ); done(); }); - }); - - it('queue continues after failures', (done) => { - const rm = new MockedRequestManager(1, 0); - const r0 = rm.request('1'); - const r1 = rm.request('2'); - rm.waitForDispatch(1).then(() => { - rm.rejectFakeRequest(); }); - r0.then( - (success) => done(new Error('r0 should have failed')), - (failure) => 'unused_argument') - .then(() => rm.resolveFakeRequest()); - - // When the first request rejects, it should decrement nActiveRequests - // and then launch remaining requests in queue (i.e. this one) - r1.then((success) => done(), (failure) => done(new Error(failure))); - }); + it('queue continues after failures', (done) => { + const rm = new MockedRequestManager(1, 0); + const r0 = rm.request('1'); + const r1 = rm.request('2'); + rm.waitForDispatch(1).then(() => { + rm.rejectFakeRequest(); + }); + + r0.then( + (success) => done(new Error('r0 should have failed')), + (failure) => 'unused_argument' + ).then(() => rm.resolveFakeRequest()); + + // When the first request rejects, it should decrement nActiveRequests + // and then launch remaining requests in queue (i.e. this one) + r1.then((success) => done(), (failure) => done(new Error(failure))); + }); - it('queue is LIFO', (done) => { - /* This test is a bit tricky. - * We want to verify that the RequestManager queue has LIFO semantics. - * So we construct three requests off the bat: A, B, C. - * So LIFO semantics ensure these will resolve in order A, C, B. - * (Because the A request launches immediately when we create it, it's - * not in queue) - * Then after resolving A, C moves out of queue, and we create X. - * So expected final order is A, C, X, B. - * We verify this with an external var that counts how many requests were - * resolved. - */ - const rm = new MockedRequestManager(1); - let nResolved = 0; - function assertResolutionOrder(expectedSpotInSequence) { - return () => { - nResolved++; - chai.assert.equal(expectedSpotInSequence, nResolved); - }; - } + it('queue is LIFO', (done) => { + /* This test is a bit tricky. + * We want to verify that the RequestManager queue has LIFO semantics. + * So we construct three requests off the bat: A, B, C. + * So LIFO semantics ensure these will resolve in order A, C, B. + * (Because the A request launches immediately when we create it, it's + * not in queue) + * Then after resolving A, C moves out of queue, and we create X. + * So expected final order is A, C, X, B. + * We verify this with an external var that counts how many requests were + * resolved. + */ + const rm = new MockedRequestManager(1); + let nResolved = 0; + function assertResolutionOrder(expectedSpotInSequence) { + return () => { + nResolved++; + chai.assert.equal(expectedSpotInSequence, nResolved); + }; + } - function launchThirdRequest() { - rm.request('started late but goes third') + function launchThirdRequest() { + rm.request('started late but goes third') .then(assertResolutionOrder(3)) .then(() => rm.dispatchAndResolve()); - } + } - rm.request('first') - .then( - assertResolutionOrder(1)) // Assert that this one resolved first + rm.request('first') + .then(assertResolutionOrder(1)) // Assert that this one resolved first .then(launchThirdRequest) - .then(() => rm.dispatchAndResolve()); // then trigger the next one + .then(() => rm.dispatchAndResolve()); // then trigger the next one - rm.request('this one goes fourth') // created second, will go last - .then(assertResolutionOrder( - 4)) // assert it was the fourth to get resolved - .then(done); // finish the test + rm.request('this one goes fourth') // created second, will go last + .then(assertResolutionOrder(4)) // assert it was the fourth to get resolved + .then(done); // finish the test - rm.request('second') + rm.request('second') .then(assertResolutionOrder(2)) .then(() => rm.dispatchAndResolve()); - rm.dispatchAndResolve(); - }); - - it('requestManager can clear queue', (done) => { - const rm = new MockedRequestManager(1); - let requestsResolved = 0; - let requestsRejected = 0; - const success = () => requestsResolved++; - const failure = (err) => { - chai.assert.equal(err.name, 'RequestCancellationError'); - requestsRejected++; - }; - const finishTheTest = () => { - chai.assert.equal(rm.activeRequests(), 0, 'no requests still active'); - chai.assert.equal( - rm.requestsDispatched, 1, 'only one req was ever dispatched'); - chai.assert.equal(rm.outstandingRequests(), 0, 'no pending requests'); - chai.assert.equal(requestsResolved, 1, 'one request got resolved'); - chai.assert.equal( - requestsRejected, 4, 'four were cancelled and threw errors'); - done(); - }; - rm.request('0').then(success, failure).then(finishTheTest); - rm.request('1').then(success, failure); - rm.request('2').then(success, failure); - rm.request('3').then(success, failure); - rm.request('4').then(success, failure); - chai.assert.equal(rm.activeRequests(), 1, 'one req is active'); - rm.waitForDispatch(1).then(() => { - chai.assert.equal(rm.activeRequests(), 1, 'one req is active'); - chai.assert.equal(rm.requestsDispatched, 1, 'one req was dispatched'); - chai.assert.equal(rm.outstandingRequests(), 5, 'five reqs outstanding'); - rm.clearQueue(); - rm.resolveFakeRequest(); - // resolving the first request triggers finishTheTest - }); - }); - - it('throws an error when a GET request has a body', function() { - const rm = new RequestManager(); - const badOptions = new RequestOptions(); - badOptions.methodType = HttpMethodType.GET; - badOptions.body = "a body"; - chai.assert.throws( - ()=>rm.requestWithOptions("http://www.google.com", badOptions), - InvalidRequestOptionsError); - }); - - describe('tests using sinon.fakeServer', function() { - let server; - - beforeEach(function() { - server = sinon.fakeServer.create(); - server.respondImmediately = true; - server.respondWith("{}"); - }); - - afterEach(function() { - server.restore(); + rm.dispatchAndResolve(); }); - it('builds correct XMLHttpRequest when request(url) is called', - function() { - const rm = new RequestManager(); - return rm.request("my_url") - .then(()=>{ - chai.assert.lengthOf(server.requests, 1); - chai.assert.equal(server.requests[0].url, "my_url"); - chai.assert.equal(server.requests[0].requestBody, null); - chai.assert.equal(server.requests[0].method, HttpMethodType.GET); - chai.assert.notProperty(server.requests[0].requestHeaders, - "Content-Type"); - }); - }); - - it('builds correct XMLHttpRequest when request(url, postData) is called', - function() { - const rm = new RequestManager(); - return rm.request("my_url", - {"key1": "value1", "key2": "value2"}) - .then(() => { - chai.assert.lengthOf(server.requests, 1); - chai.assert.equal(server.requests[0].url, "my_url"); - chai.assert.equal(server.requests[0].method, - HttpMethodType.POST); - chai.assert.instanceOf(server.requests[0].requestBody, FormData); - chai.assert.sameDeepMembers( - Array.from(server.requests[0].requestBody.entries()), - [["key1", "value1"], ["key2", "value2"]]); - }); - }); - - it('builds correct XMLHttpRequest when requestWithOptions is called', - function() { - const rm = new RequestManager(); - const requestOptions = new RequestOptions(); - requestOptions.methodType = HttpMethodType.POST; - requestOptions.contentType = "text/plain;charset=utf-8"; - requestOptions.body = "the body"; - return rm.requestWithOptions("my_url", requestOptions) - .then(()=>{ - chai.assert.lengthOf(server.requests, 1); - chai.assert.equal(server.requests[0].url, "my_url"); - chai.assert.equal(server.requests[0].method, - HttpMethodType.POST); - chai.assert.equal(server.requests[0].requestBody, "the body"); - chai.assert.equal( - server.requests[0].requestHeaders["Content-Type"], - "text/plain;charset=utf-8"); - }); - }); - }); - - describe('fetch', () => { - beforeEach(function() { - this.stubbedFetch = sandbox.stub(window, 'fetch'); - this.clock = sandbox.useFakeTimers(); - - this.resolvesAfter = function(value: any, timeInMs: number): - Promise { - return new Promise((resolve) => { - setTimeout(() => resolve(value), timeInMs); - }); - } - }); - - it('resolves', async function() { - this.stubbedFetch.returns( - Promise.resolve(new Response('Success', {status: 200}))); - const rm = new RequestManager(); - - const response = await rm.fetch('foo'); - - expect(response).to.have.property('ok', true); - expect(response).to.have.property('status', 200); - const body = await response.text(); - expect(body).to.equal('Success'); - }); - - it('retries', async function() { - this.stubbedFetch.onCall(0).returns( - Promise.resolve(new Response('Error 1', {status: 500}))); - this.stubbedFetch.onCall(1).returns( - Promise.resolve(new Response('Error 2', {status: 500}))); - this.stubbedFetch.onCall(2).returns( - Promise.resolve(new Response('Success', {status: 200}))); - const rm = new RequestManager(); - - const response = await rm.fetch('foo'); - - expect(response).to.have.property('ok', true); - expect(response).to.have.property('status', 200); - const body = await response.text(); - expect(body).to.equal('Success'); + it('requestManager can clear queue', (done) => { + const rm = new MockedRequestManager(1); + let requestsResolved = 0; + let requestsRejected = 0; + const success = () => requestsResolved++; + const failure = (err) => { + chai.assert.equal(err.name, 'RequestCancellationError'); + requestsRejected++; + }; + const finishTheTest = () => { + chai.assert.equal(rm.activeRequests(), 0, 'no requests still active'); + chai.assert.equal( + rm.requestsDispatched, + 1, + 'only one req was ever dispatched' + ); + chai.assert.equal(rm.outstandingRequests(), 0, 'no pending requests'); + chai.assert.equal(requestsResolved, 1, 'one request got resolved'); + chai.assert.equal( + requestsRejected, + 4, + 'four were cancelled and threw errors' + ); + done(); + }; + rm.request('0') + .then(success, failure) + .then(finishTheTest); + rm.request('1').then(success, failure); + rm.request('2').then(success, failure); + rm.request('3').then(success, failure); + rm.request('4').then(success, failure); + chai.assert.equal(rm.activeRequests(), 1, 'one req is active'); + rm.waitForDispatch(1).then(() => { + chai.assert.equal(rm.activeRequests(), 1, 'one req is active'); + chai.assert.equal(rm.requestsDispatched, 1, 'one req was dispatched'); + chai.assert.equal( + rm.outstandingRequests(), + 5, + 'five reqs outstanding' + ); + rm.clearQueue(); + rm.resolveFakeRequest(); + // resolving the first request triggers finishTheTest + }); }); - it('gives up after max retries', async function() { - const failure = new Response('Error', {status: 500}); - this.stubbedFetch.returns(Promise.resolve(failure)); + it('throws an error when a GET request has a body', function() { const rm = new RequestManager(); - - const response = await rm.fetch('foo'); - - expect(this.stubbedFetch).to.have.been.calledThrice; - expect(response).to.have.property('ok', false); - expect(response).to.have.property('status', 500); - const body = await response.text(); - expect(body).to.equal('Error'); + const badOptions = new RequestOptions(); + badOptions.methodType = HttpMethodType.GET; + badOptions.body = 'a body'; + chai.assert.throws( + () => rm.requestWithOptions('http://www.google.com', badOptions), + InvalidRequestOptionsError + ); }); - it('sends requests concurrently', async function() { - this.stubbedFetch.onCall(0).returns( - this.resolvesAfter(new Response('nay', {status: 200}), 3000)); - this.stubbedFetch.onCall(1).returns( - Promise.resolve(new Response('yay', {status: 200}))); - - const rm = new RequestManager(/** nSimultaneousRequests */ 2); - - const promise1 = rm.fetch('foo'); - const promise2 = rm.fetch('bar'); - - const secondResponse = await Promise.race([promise1, promise2]); - const secondBody = await secondResponse.text(); - expect(secondBody).to.equal('yay'); - - this.clock.tick(3000); - - const firstResponse = await promise1; - const firstBody = await firstResponse.text(); - expect(firstBody).to.equal('nay'); + describe('tests using sinon.fakeServer', function() { + let server; + + beforeEach(function() { + server = sinon.fakeServer.create(); + server.respondImmediately = true; + server.respondWith('{}'); + }); + + afterEach(function() { + server.restore(); + }); + + it('builds correct XMLHttpRequest when request(url) is called', function() { + const rm = new RequestManager(); + return rm.request('my_url').then(() => { + chai.assert.lengthOf(server.requests, 1); + chai.assert.equal(server.requests[0].url, 'my_url'); + chai.assert.equal(server.requests[0].requestBody, null); + chai.assert.equal(server.requests[0].method, HttpMethodType.GET); + chai.assert.notProperty( + server.requests[0].requestHeaders, + 'Content-Type' + ); + }); + }); + + it('builds correct XMLHttpRequest when request(url, postData) is called', function() { + const rm = new RequestManager(); + return rm + .request('my_url', {key1: 'value1', key2: 'value2'}) + .then(() => { + chai.assert.lengthOf(server.requests, 1); + chai.assert.equal(server.requests[0].url, 'my_url'); + chai.assert.equal(server.requests[0].method, HttpMethodType.POST); + chai.assert.instanceOf(server.requests[0].requestBody, FormData); + chai.assert.sameDeepMembers( + Array.from(server.requests[0].requestBody.entries()), + [['key1', 'value1'], ['key2', 'value2']] + ); + }); + }); + + it('builds correct XMLHttpRequest when requestWithOptions is called', function() { + const rm = new RequestManager(); + const requestOptions = new RequestOptions(); + requestOptions.methodType = HttpMethodType.POST; + requestOptions.contentType = 'text/plain;charset=utf-8'; + requestOptions.body = 'the body'; + return rm.requestWithOptions('my_url', requestOptions).then(() => { + chai.assert.lengthOf(server.requests, 1); + chai.assert.equal(server.requests[0].url, 'my_url'); + chai.assert.equal(server.requests[0].method, HttpMethodType.POST); + chai.assert.equal(server.requests[0].requestBody, 'the body'); + chai.assert.equal( + server.requests[0].requestHeaders['Content-Type'], + 'text/plain;charset=utf-8' + ); + }); + }); }); - it('queues requests', async function() { - this.stubbedFetch.onCall(0).returns( - this.resolvesAfter(new Response('nay', {status: 200}), 3000)); - this.stubbedFetch.onCall(1).returns( - Promise.resolve(new Response('yay', {status: 200}))); - - - const rm = new RequestManager(/** nSimultaneousRequests */ 1); - - const promise1 = rm.fetch('foo'); - const promise2 = rm.fetch('bar'); - - expect(rm.activeRequests()).to.equal(1); - expect(rm.outstandingRequests()).to.equal(2); - - this.clock.tick(3000); - - const firstResponse = await Promise.race([promise1, promise2]); - const firstBody = await firstResponse.text(); - expect(firstBody).to.equal('nay'); - - const secondResponse = await promise2; - const secondBody = await secondResponse.text(); - expect(secondBody).to.equal('yay'); + describe('fetch', () => { + beforeEach(function() { + this.stubbedFetch = sandbox.stub(window, 'fetch'); + this.clock = sandbox.useFakeTimers(); + + this.resolvesAfter = function( + value: any, + timeInMs: number + ): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve(value), timeInMs); + }); + }; + }); + + it('resolves', async function() { + this.stubbedFetch.returns( + Promise.resolve(new Response('Success', {status: 200})) + ); + const rm = new RequestManager(); + + const response = await rm.fetch('foo'); + + expect(response).to.have.property('ok', true); + expect(response).to.have.property('status', 200); + const body = await response.text(); + expect(body).to.equal('Success'); + }); + + it('retries', async function() { + this.stubbedFetch + .onCall(0) + .returns(Promise.resolve(new Response('Error 1', {status: 500}))); + this.stubbedFetch + .onCall(1) + .returns(Promise.resolve(new Response('Error 2', {status: 500}))); + this.stubbedFetch + .onCall(2) + .returns(Promise.resolve(new Response('Success', {status: 200}))); + const rm = new RequestManager(); + + const response = await rm.fetch('foo'); + + expect(response).to.have.property('ok', true); + expect(response).to.have.property('status', 200); + const body = await response.text(); + expect(body).to.equal('Success'); + }); + + it('gives up after max retries', async function() { + const failure = new Response('Error', {status: 500}); + this.stubbedFetch.returns(Promise.resolve(failure)); + const rm = new RequestManager(); + + const response = await rm.fetch('foo'); + + expect(this.stubbedFetch).to.have.been.calledThrice; + expect(response).to.have.property('ok', false); + expect(response).to.have.property('status', 500); + const body = await response.text(); + expect(body).to.equal('Error'); + }); + + it('sends requests concurrently', async function() { + this.stubbedFetch + .onCall(0) + .returns( + this.resolvesAfter(new Response('nay', {status: 200}), 3000) + ); + this.stubbedFetch + .onCall(1) + .returns(Promise.resolve(new Response('yay', {status: 200}))); + + const rm = new RequestManager(/** nSimultaneousRequests */ 2); + + const promise1 = rm.fetch('foo'); + const promise2 = rm.fetch('bar'); + + const secondResponse = await Promise.race([promise1, promise2]); + const secondBody = await secondResponse.text(); + expect(secondBody).to.equal('yay'); + + this.clock.tick(3000); + + const firstResponse = await promise1; + const firstBody = await firstResponse.text(); + expect(firstBody).to.equal('nay'); + }); + + it('queues requests', async function() { + this.stubbedFetch + .onCall(0) + .returns( + this.resolvesAfter(new Response('nay', {status: 200}), 3000) + ); + this.stubbedFetch + .onCall(1) + .returns(Promise.resolve(new Response('yay', {status: 200}))); + + const rm = new RequestManager(/** nSimultaneousRequests */ 1); + + const promise1 = rm.fetch('foo'); + const promise2 = rm.fetch('bar'); + + expect(rm.activeRequests()).to.equal(1); + expect(rm.outstandingRequests()).to.equal(2); + + this.clock.tick(3000); + + const firstResponse = await Promise.race([promise1, promise2]); + const firstBody = await firstResponse.text(); + expect(firstBody).to.equal('nay'); + + const secondResponse = await promise2; + const secondBody = await secondResponse.text(); + expect(secondBody).to.equal('yay'); + }); }); }); }); -}); - -} // namespace tf_backend +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/test/tests.html b/tensorboard/components/tf_backend/test/tests.html index fe8092388f..899c9818cb 100644 --- a/tensorboard/components/tf_backend/test/tests.html +++ b/tensorboard/components/tf_backend/test/tests.html @@ -1,4 +1,4 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/tensorboard/components/tf_backend/tf-backend.html b/tensorboard/components/tf_backend/tf-backend.html index f87a77ed9a..d32afdb50e 100644 --- a/tensorboard/components/tf_backend/tf-backend.html +++ b/tensorboard/components/tf_backend/tf-backend.html @@ -15,8 +15,8 @@ limitations under the License. --> - - + + diff --git a/tensorboard/components/tf_backend/type.ts b/tensorboard/components/tf_backend/type.ts index a82361b9c0..6130f5c713 100644 --- a/tensorboard/components/tf_backend/type.ts +++ b/tensorboard/components/tf_backend/type.ts @@ -13,25 +13,23 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { - -export type ExperimentId = number; -export type RunId = number | null; -export type TagId = number; - -export type Experiment = {id: ExperimentId, name: string, startTime: number}; - -export type Run = { - id: RunId, - name: string, - startTime: number, - tags: Tag[], -}; - -export type Tag = { - id: TagId, - name: string, - displayName: string, - pluginName: string, -}; - -} // namespace tf_backend + export type ExperimentId = number; + export type RunId = number | null; + export type TagId = number; + + export type Experiment = {id: ExperimentId; name: string; startTime: number}; + + export type Run = { + id: RunId; + name: string; + startTime: number; + tags: Tag[]; + }; + + export type Tag = { + id: TagId; + name: string; + displayName: string; + pluginName: string; + }; +} // namespace tf_backend diff --git a/tensorboard/components/tf_backend/urlPathHelpers.ts b/tensorboard/components/tf_backend/urlPathHelpers.ts index e98ca4d672..4cb4a96807 100644 --- a/tensorboard/components/tf_backend/urlPathHelpers.ts +++ b/tensorboard/components/tf_backend/urlPathHelpers.ts @@ -13,48 +13,52 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_backend { + /** + * A query parameter value can either be a string or a list of strings. + * A string `"foo"` is encoded as `key=foo`; a list `["foo", "bar"]` is + * encoded as `key=foo&key=bar`. + */ + export type QueryValue = string | string[]; -/** - * A query parameter value can either be a string or a list of strings. - * A string `"foo"` is encoded as `key=foo`; a list `["foo", "bar"]` is - * encoded as `key=foo&key=bar`. - */ -export type QueryValue = string | string[]; + export type QueryParams = {[key: string]: QueryValue}; -export type QueryParams = {[key: string]: QueryValue}; - -/** - * Add query parameters to a URL. Values will be URL-encoded. The URL - * may or may not already have query parameters. For convenience, - * parameters whose value is `undefined` will be dropped. - * - * For example, the following expressions are equivalent: - * - * addParams("http://foo", {a: "1", b: ["2", "3+4"], c: "5"}) - * addParams("http://foo?a=1", {b: ["2", "3+4"], c: "5", d: undefined}) - * "http://foo?a=1&b=2&b=3%2B4&c=5" - * - * @deprecated If used with `router.pluginRoute`, please use the queryParams - * argument. - */ -export function addParams(baseURL: string, params: QueryParams): string { - const keys = Object.keys(params).sort().filter(k => params[k] !== undefined); - if (!keys.length) { - return baseURL; // no need to change '/foo' to '/foo?' + /** + * Add query parameters to a URL. Values will be URL-encoded. The URL + * may or may not already have query parameters. For convenience, + * parameters whose value is `undefined` will be dropped. + * + * For example, the following expressions are equivalent: + * + * addParams("http://foo", {a: "1", b: ["2", "3+4"], c: "5"}) + * addParams("http://foo?a=1", {b: ["2", "3+4"], c: "5", d: undefined}) + * "http://foo?a=1&b=2&b=3%2B4&c=5" + * + * @deprecated If used with `router.pluginRoute`, please use the queryParams + * argument. + */ + export function addParams(baseURL: string, params: QueryParams): string { + const keys = Object.keys(params) + .sort() + .filter((k) => params[k] !== undefined); + if (!keys.length) { + return baseURL; // no need to change '/foo' to '/foo?' + } + const delimiter = baseURL.indexOf('?') !== -1 ? '&' : '?'; + const parts = [].concat( + ...keys.map((key) => { + const rawValue = params[key]; + const values = Array.isArray(rawValue) ? rawValue : [rawValue]; + return values.map((value) => `${key}=${_encodeURIComponent(value)}`); + }) + ); + const query = parts.join('&'); + return baseURL + delimiter + query; } - const delimiter = baseURL.indexOf('?') !== -1 ? '&' : '?'; - const parts = [].concat(...keys.map(key => { - const rawValue = params[key]; - const values = Array.isArray(rawValue) ? rawValue : [rawValue]; - return values.map(value => `${key}=${_encodeURIComponent(value)}`); - })); - const query = parts.join('&'); - return baseURL + delimiter + query; -} - -function _encodeURIComponent(x: string): string { - // Replace parentheses for consistency with Python's urllib.urlencode. - return encodeURIComponent(x).replace(/\(/g, '%28').replace(/\)/g, '%29'); -} -} // namespace tf_backend + function _encodeURIComponent(x: string): string { + // Replace parentheses for consistency with Python's urllib.urlencode. + return encodeURIComponent(x) + .replace(/\(/g, '%28') + .replace(/\)/g, '%29'); + } +} // namespace tf_backend diff --git a/tensorboard/components/tf_card_heading/tf-card-heading-style.html b/tensorboard/components/tf_card_heading/tf-card-heading-style.html index ff165a8fa2..d725b25d82 100644 --- a/tensorboard/components/tf_card_heading/tf-card-heading-style.html +++ b/tensorboard/components/tf_card_heading/tf-card-heading-style.html @@ -21,7 +21,6 @@ diff --git a/tensorboard/components/tf_dashboard_common/scrollbar-style.html b/tensorboard/components/tf_dashboard_common/scrollbar-style.html index a1b00a4208..bd4c9bd736 100644 --- a/tensorboard/components/tf_dashboard_common/scrollbar-style.html +++ b/tensorboard/components/tf_dashboard_common/scrollbar-style.html @@ -15,26 +15,23 @@ limitations under the License. --> - - + + diff --git a/tensorboard/components/tf_storage/listeners.ts b/tensorboard/components/tf_storage/listeners.ts index e445d4eeaf..f7f6eb540f 100644 --- a/tensorboard/components/tf_storage/listeners.ts +++ b/tensorboard/components/tf_storage/listeners.ts @@ -13,52 +13,50 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_storage { - -// TODO(stephanwlee): Combine this with tf_backend.ListenKey and put it in a -// sensible place. -// A unique reference to a listener for an easier dereferencing. -export class ListenKey { - public readonly listener: Function; - constructor(listener: Function) { - this.listener = listener; + // TODO(stephanwlee): Combine this with tf_backend.ListenKey and put it in a + // sensible place. + // A unique reference to a listener for an easier dereferencing. + export class ListenKey { + public readonly listener: Function; + constructor(listener: Function) { + this.listener = listener; + } } -} - -const hashListeners = new Set(); -const storageListeners = new Set(); -window.addEventListener('hashchange', () => { - hashListeners.forEach(listenKey => listenKey.listener()); -}); + const hashListeners = new Set(); + const storageListeners = new Set(); -// [1]: The event only triggers when another tab edits the storage. Changing a -// value in current browser tab will NOT trigger below event. -window.addEventListener('storage', () => { - storageListeners.forEach(listenKey => listenKey.listener()); -}); + window.addEventListener('hashchange', () => { + hashListeners.forEach((listenKey) => listenKey.listener()); + }); -export function addHashListener(fn: Function): ListenKey { - const key = new ListenKey(fn); - hashListeners.add(key); - return key; -} + // [1]: The event only triggers when another tab edits the storage. Changing a + // value in current browser tab will NOT trigger below event. + window.addEventListener('storage', () => { + storageListeners.forEach((listenKey) => listenKey.listener()); + }); -export function addStorageListener(fn: Function): ListenKey { - const key = new ListenKey(fn); - storageListeners.add(key); - return key; -} + export function addHashListener(fn: Function): ListenKey { + const key = new ListenKey(fn); + hashListeners.add(key); + return key; + } -export function fireStorageChanged() { - storageListeners.forEach(listenKey => listenKey.listener()); -} + export function addStorageListener(fn: Function): ListenKey { + const key = new ListenKey(fn); + storageListeners.add(key); + return key; + } -export function removeHashListenerByKey(key: ListenKey) { - hashListeners.delete(key); -} + export function fireStorageChanged() { + storageListeners.forEach((listenKey) => listenKey.listener()); + } -export function removeStorageListenerByKey(key: ListenKey) { - storageListeners.delete(key); -} + export function removeHashListenerByKey(key: ListenKey) { + hashListeners.delete(key); + } -} // namespace tf_storage + export function removeStorageListenerByKey(key: ListenKey) { + storageListeners.delete(key); + } +} // namespace tf_storage diff --git a/tensorboard/components/tf_storage/storage.ts b/tensorboard/components/tf_storage/storage.ts index 978c73fc24..ac679c5110 100644 --- a/tensorboard/components/tf_storage/storage.ts +++ b/tensorboard/components/tf_storage/storage.ts @@ -13,303 +13,297 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_storage { + /** + * The Storage Module provides storage for persisting state in URI or + * localStorage. + * + * When using URI as the storage mechanism, it generates URI components like: + * #events&runPrefix=train*. + * It also allows saving the values to localStorage for non-noisy and larger + * data persistence. + */ + type StringDict = {[key: string]: string}; -/** - * The Storage Module provides storage for persisting state in URI or - * localStorage. - * - * When using URI as the storage mechanism, it generates URI components like: - * #events&runPrefix=train*. - * It also allows saving the values to localStorage for non-noisy and larger - * data persistence. - */ -type StringDict = {[key: string]: string}; - -/** - * A keyword that users cannot use, since TensorBoard uses this to store info - * about the active tab. - */ -export const TAB = '__tab__'; - -/** - * The name of the property for users to set on a Polymer component - * in order for its stored properties to be stored in the URI unambiguously. - * (No need to set this if you want multiple instances of the component to - * share URI state) - * - * Example: - * - * - * The disambiguator should be set to any unique value so that multiple - * instances of the component can store properties in URI storage. - * - * Because it's hard to dereference this variable in HTML property bindings, - * it is NOT safe to change the disambiguator string without find+replace - * across the codebase. - */ -export const DISAMBIGUATOR = 'disambiguator'; - -export const { - get: getString, - set: setString, - getInitializer: getStringInitializer, - getObserver: getStringObserver, - disposeBinding: disposeStringBinding, -} = makeBindings(x => x, x => x); - -export const { - get: getBoolean, - set: setBoolean, - getInitializer: getBooleanInitializer, - getObserver: getBooleanObserver, - disposeBinding: disposeBooleanBinding, -} = makeBindings( - s => (s === 'true' ? true: s === 'false' ? false : undefined), - b => b.toString()); - -export const { - get: getNumber, - set: setNumber, - getInitializer: getNumberInitializer, - getObserver: getNumberObserver, - disposeBinding: disposeNumberBinding, -} = makeBindings( - s => +s, - n => n.toString()); - -export const { - get: getObject, - set: setObject, - getInitializer: getObjectInitializer, - getObserver: getObjectObserver, - disposeBinding: disposeObjectBinding, -} = makeBindings( - s => JSON.parse(atob(s)), - o => btoa(JSON.stringify(o))); - -export interface StorageOptions { - defaultValue?: T; - useLocalStorage?: boolean; -} - -export interface AutoStorageOptions extends StorageOptions { - polymerProperty?: string, -} - -export interface SetterOptions extends StorageOptions { - defaultValue?: T; - useLocalStorage?: boolean; - useLocationReplace?: boolean; -} - -export function makeBindings(fromString: (string) => T, toString: (T) => string): { - get: (key: string, option?: StorageOptions) => T, - set: (key: string, value: T, option?: SetterOptions) => void, - getInitializer: (key: string, options: AutoStorageOptions) => Function, - getObserver: (key: string, options: AutoStorageOptions) => Function, - disposeBinding: () => void, -} { - const hashListeners = []; - const storageListeners = []; - - function get(key: string, options: StorageOptions = {}): T { - const { - defaultValue, - useLocalStorage = false, - } = options; - const value = useLocalStorage ? - window.localStorage.getItem(key) : - componentToDict(readComponent())[key]; - return value == undefined ? _.cloneDeep(defaultValue) : fromString(value); + /** + * A keyword that users cannot use, since TensorBoard uses this to store info + * about the active tab. + */ + export const TAB = '__tab__'; + + /** + * The name of the property for users to set on a Polymer component + * in order for its stored properties to be stored in the URI unambiguously. + * (No need to set this if you want multiple instances of the component to + * share URI state) + * + * Example: + * + * + * The disambiguator should be set to any unique value so that multiple + * instances of the component can store properties in URI storage. + * + * Because it's hard to dereference this variable in HTML property bindings, + * it is NOT safe to change the disambiguator string without find+replace + * across the codebase. + */ + export const DISAMBIGUATOR = 'disambiguator'; + + export const { + get: getString, + set: setString, + getInitializer: getStringInitializer, + getObserver: getStringObserver, + disposeBinding: disposeStringBinding, + } = makeBindings((x) => x, (x) => x); + + export const { + get: getBoolean, + set: setBoolean, + getInitializer: getBooleanInitializer, + getObserver: getBooleanObserver, + disposeBinding: disposeBooleanBinding, + } = makeBindings( + (s) => (s === 'true' ? true : s === 'false' ? false : undefined), + (b) => b.toString() + ); + + export const { + get: getNumber, + set: setNumber, + getInitializer: getNumberInitializer, + getObserver: getNumberObserver, + disposeBinding: disposeNumberBinding, + } = makeBindings((s) => +s, (n) => n.toString()); + + export const { + get: getObject, + set: setObject, + getInitializer: getObjectInitializer, + getObserver: getObjectObserver, + disposeBinding: disposeObjectBinding, + } = makeBindings((s) => JSON.parse(atob(s)), (o) => btoa(JSON.stringify(o))); + + export interface StorageOptions { + defaultValue?: T; + useLocalStorage?: boolean; } - function set(key: string, value: T, options: SetterOptions = {}): void { - const { - defaultValue, - useLocalStorage = false, - useLocationReplace = false, - } = options; - const stringValue = toString(value); - if (useLocalStorage) { - window.localStorage.setItem(key, stringValue); - // Because of listeners.ts:[1], we need to manually notify all UI elements - // listening to storage within the tab of a change. - fireStorageChanged(); - } else if (!_.isEqual(value, get(key, {useLocalStorage}))) { - if (_.isEqual(value, defaultValue)) { - unsetFromURI(key); - } else { - const items = componentToDict(readComponent()); - items[key] = stringValue; - writeComponent(dictToComponent(items), useLocationReplace); + export interface AutoStorageOptions extends StorageOptions { + polymerProperty?: string; + } + + export interface SetterOptions extends StorageOptions { + defaultValue?: T; + useLocalStorage?: boolean; + useLocationReplace?: boolean; + } + + export function makeBindings( + fromString: (string) => T, + toString: (T) => string + ): { + get: (key: string, option?: StorageOptions) => T; + set: (key: string, value: T, option?: SetterOptions) => void; + getInitializer: (key: string, options: AutoStorageOptions) => Function; + getObserver: (key: string, options: AutoStorageOptions) => Function; + disposeBinding: () => void; + } { + const hashListeners = []; + const storageListeners = []; + + function get(key: string, options: StorageOptions = {}): T { + const {defaultValue, useLocalStorage = false} = options; + const value = useLocalStorage + ? window.localStorage.getItem(key) + : componentToDict(readComponent())[key]; + return value == undefined ? _.cloneDeep(defaultValue) : fromString(value); + } + + function set(key: string, value: T, options: SetterOptions = {}): void { + const { + defaultValue, + useLocalStorage = false, + useLocationReplace = false, + } = options; + const stringValue = toString(value); + if (useLocalStorage) { + window.localStorage.setItem(key, stringValue); + // Because of listeners.ts:[1], we need to manually notify all UI elements + // listening to storage within the tab of a change. + fireStorageChanged(); + } else if (!_.isEqual(value, get(key, {useLocalStorage}))) { + if (_.isEqual(value, defaultValue)) { + unsetFromURI(key); + } else { + const items = componentToDict(readComponent()); + items[key] = stringValue; + writeComponent(dictToComponent(items), useLocationReplace); + } } } - } - /** - * Returns a function that can be used on a `value` declaration to a Polymer - * property. It updates the `polymerProperty` when storage changes -- i.e., - * when `useLocalStorage`, it listens to storage change from another tab and - * when `useLocalStorage=false`, it listens to hashchange. - */ - function getInitializer(key: string, options: StorageOptions): Function { - const fullOptions = { - defaultValue: options.defaultValue, - polymerProperty: key, - useLocalStorage: false, - ...options, - }; - return function() { - const uriStorageName = getURIStorageName(this, key); - // setComponentValue will be called every time the underlying storage - // changes and is responsible for ensuring that new state will propagate - // to the component with specified property. It is important that this - // function does not re-assign needlessly, to avoid Polymer observer - // churn. - const setComponentValue = () => { - const storedValue = get(uriStorageName, fullOptions); - const currentValue = this[fullOptions.polymerProperty]; - if (!_.isEqual(storedValue, currentValue)) { - this[fullOptions.polymerProperty] = storedValue; + /** + * Returns a function that can be used on a `value` declaration to a Polymer + * property. It updates the `polymerProperty` when storage changes -- i.e., + * when `useLocalStorage`, it listens to storage change from another tab and + * when `useLocalStorage=false`, it listens to hashchange. + */ + function getInitializer(key: string, options: StorageOptions): Function { + const fullOptions = { + defaultValue: options.defaultValue, + polymerProperty: key, + useLocalStorage: false, + ...options, + }; + return function() { + const uriStorageName = getURIStorageName(this, key); + // setComponentValue will be called every time the underlying storage + // changes and is responsible for ensuring that new state will propagate + // to the component with specified property. It is important that this + // function does not re-assign needlessly, to avoid Polymer observer + // churn. + const setComponentValue = () => { + const storedValue = get(uriStorageName, fullOptions); + const currentValue = this[fullOptions.polymerProperty]; + if (!_.isEqual(storedValue, currentValue)) { + this[fullOptions.polymerProperty] = storedValue; + } + }; + + const addListener = fullOptions.useLocalStorage + ? addStorageListener + : addHashListener; + + // TODO(stephanwlee): When using fakeHash, it _should not_ listen to the + // window.hashchange. + const listenKey = addListener(() => setComponentValue()); + if (fullOptions.useLocalStorage) { + storageListeners.push(listenKey); + } else { + hashListeners.push(listenKey); } + + // Set the value on the property. + setComponentValue(); + return this[fullOptions.polymerProperty]; }; + } - const addListener = fullOptions.useLocalStorage ? - addStorageListener : - addHashListener; + function disposeBinding() { + hashListeners.forEach((key) => removeHashListenerByKey(key)); + storageListeners.forEach((key) => removeStorageListenerByKey(key)); + } - // TODO(stephanwlee): When using fakeHash, it _should not_ listen to the - // window.hashchange. - const listenKey = addListener(() => setComponentValue()); - if (fullOptions.useLocalStorage) { - storageListeners.push(listenKey); - } else { - hashListeners.push(listenKey); - } + function getObserver(key: string, options: StorageOptions): Function { + const fullOptions = { + defaultValue: options.defaultValue, + polymerProperty: key, + useLocalStorage: false, + ...options, + }; + return function() { + const uriStorageName = getURIStorageName(this, key); + const newVal = this[fullOptions.polymerProperty]; + set(uriStorageName, newVal, fullOptions); + }; + } - // Set the value on the property. - setComponentValue(); - return this[fullOptions.polymerProperty]; - }; + return {get, set, getInitializer, getObserver, disposeBinding}; } - function disposeBinding() { - hashListeners.forEach(key => removeHashListenerByKey(key)); - storageListeners.forEach(key => removeStorageListenerByKey(key)); + /** + * Get a unique storage name for a (Polymer component, propertyName) tuple. + * + * DISAMBIGUATOR must be set on the component, if other components use the + * same propertyName. + */ + function getURIStorageName(component: {}, propertyName: string): string { + const d = component[DISAMBIGUATOR]; + const components = d == null ? [propertyName] : [d, propertyName]; + return components.join('.'); } - function getObserver(key: string, options: StorageOptions): Function { - const fullOptions = { - defaultValue: options.defaultValue, - polymerProperty: key, - useLocalStorage: false, - ...options, - }; - return function() { - const uriStorageName = getURIStorageName(this, key); - const newVal = this[fullOptions.polymerProperty]; - set(uriStorageName, newVal, fullOptions); - }; + /** + * Read component from URI (e.g. returns "events&runPrefix=train*"). + */ + function readComponent(): string { + return tf_globals.useHash() + ? window.location.hash.slice(1) + : tf_globals.getFakeHash(); } - return {get, set, getInitializer, getObserver, disposeBinding}; -} - -/** - * Get a unique storage name for a (Polymer component, propertyName) tuple. - * - * DISAMBIGUATOR must be set on the component, if other components use the - * same propertyName. - */ -function getURIStorageName( - component: {}, propertyName: string): string { - const d = component[DISAMBIGUATOR]; - const components = d == null ? [propertyName] : [d, propertyName]; - return components.join('.'); -} - - -/** - * Read component from URI (e.g. returns "events&runPrefix=train*"). - */ -function readComponent(): string { - return tf_globals.useHash() ? window.location.hash.slice(1) : tf_globals.getFakeHash(); -} - -/** - * Write component to URI. - */ -function writeComponent(component: string, useLocationReplace = false) { - if (tf_globals.useHash()) { - if (useLocationReplace) { - window.location.replace('#' + component); + /** + * Write component to URI. + */ + function writeComponent(component: string, useLocationReplace = false) { + if (tf_globals.useHash()) { + if (useLocationReplace) { + window.location.replace('#' + component); + } else { + window.location.hash = component; + } } else { - window.location.hash = component; + tf_globals.setFakeHash(component); } - } else { - tf_globals.setFakeHash(component); - } -} - -/** - * Convert dictionary of strings into a URI Component. - * All key value entries get added as key value pairs in the component, - * with the exception of a key with the TAB value, which if present - * gets prepended to the URI Component string for backwards compatibility - * reasons. - */ -function dictToComponent(items: StringDict): string { - let component = ''; - - // Add the tab name e.g. 'events', 'images', 'histograms' as a prefix - // for backwards compatbility. - if (items[TAB] !== undefined) { - component += items[TAB]; } - // Join other strings with &key=value notation - const nonTab = Object.keys(items) - .map(key => [key, items[key]]) - .filter((pair) => pair[0] !== TAB) + /** + * Convert dictionary of strings into a URI Component. + * All key value entries get added as key value pairs in the component, + * with the exception of a key with the TAB value, which if present + * gets prepended to the URI Component string for backwards compatibility + * reasons. + */ + function dictToComponent(items: StringDict): string { + let component = ''; + + // Add the tab name e.g. 'events', 'images', 'histograms' as a prefix + // for backwards compatbility. + if (items[TAB] !== undefined) { + component += items[TAB]; + } + + // Join other strings with &key=value notation + const nonTab = Object.keys(items) + .map((key) => [key, items[key]]) + .filter((pair) => pair[0] !== TAB) .map((pair) => { - return encodeURIComponent(pair[0]) + '=' + - encodeURIComponent(pair[1]); + return encodeURIComponent(pair[0]) + '=' + encodeURIComponent(pair[1]); }) .join('&'); - return nonTab.length > 0 ? (component + '&' + nonTab) : component; -} - -/** - * Convert a URI Component into a dictionary of strings. - * Component should consist of key-value pairs joined by a delimiter - * with the exception of the tabName. - * Returns dict consisting of all key-value pairs and - * dict[TAB] = tabName - */ -function componentToDict(component: string): StringDict { - const items = {} as StringDict; - - const tokens = component.split('&'); - tokens.forEach((token) => { - const kv = token.split('='); - // Special backwards compatibility for URI components like #scalars. - if (kv.length === 1) { - items[TAB] = kv[0]; - } else if (kv.length === 2) { - items[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]); - } - }); - return items; -} - -/** - * Delete a key from the URI. - */ -function unsetFromURI(key) { - const items = componentToDict(readComponent()); - delete items[key]; - writeComponent(dictToComponent(items)); -} - -} // namespace tf_storage + return nonTab.length > 0 ? component + '&' + nonTab : component; + } + + /** + * Convert a URI Component into a dictionary of strings. + * Component should consist of key-value pairs joined by a delimiter + * with the exception of the tabName. + * Returns dict consisting of all key-value pairs and + * dict[TAB] = tabName + */ + function componentToDict(component: string): StringDict { + const items = {} as StringDict; + + const tokens = component.split('&'); + tokens.forEach((token) => { + const kv = token.split('='); + // Special backwards compatibility for URI components like #scalars. + if (kv.length === 1) { + items[TAB] = kv[0]; + } else if (kv.length === 2) { + items[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]); + } + }); + return items; + } + + /** + * Delete a key from the URI. + */ + function unsetFromURI(key) { + const items = componentToDict(readComponent()); + delete items[key]; + writeComponent(dictToComponent(items)); + } +} // namespace tf_storage diff --git a/tensorboard/components/tf_storage/test/storageTests.ts b/tensorboard/components/tf_storage/test/storageTests.ts index 6cf212b05a..ca8ae0b85c 100644 --- a/tensorboard/components/tf_storage/test/storageTests.ts +++ b/tensorboard/components/tf_storage/test/storageTests.ts @@ -13,169 +13,165 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_storage { + const {assert} = chai; + + function setHash(hash) { + tf_globals.setFakeHash(hash); + } + + function getHash(): string { + return tf_globals.getFakeHash(); + } + + /* tslint:disable:no-namespace */ + describe('Storage', () => { + const option = {useLocalStorage: false}; + + afterEach(() => { + setHash(''); + window.localStorage.clear(); + disposeStringBinding(); + disposeNumberBinding(); + disposeBooleanBinding(); + disposeObjectBinding(); + }); -const {assert} = chai; - - -function setHash(hash) { - tf_globals.setFakeHash(hash); -} - -function getHash(): string { - return tf_globals.getFakeHash(); -} + it('get/setString', () => { + setString('key_a', 'hello', option); + setString('key_b', 'there', option); + assert.equal('hello', getString('key_a', option)); + assert.equal('there', getString('key_b', option)); + assert.equal(null, getString('key_c', option)); + }); + it('get/setNumber', () => { + setNumber('key_a', 12, option); + setNumber('key_b', 3.4, option); + assert.equal(12, getNumber('key_a', option)); + assert.equal(3.4, getNumber('key_b', option)); + assert.equal(null, getNumber('key_c', option)); + }); -/* tslint:disable:no-namespace */ -describe('Storage', () => { - const option = {useLocalStorage: false}; + it('get/setObject', () => { + const obj = {foo: 2.3, bar: 'barstr'}; + setObject('key_a', obj, option); + assert.deepEqual(obj, getObject('key_a', option)); + }); - afterEach(() => { - setHash(''); - window.localStorage.clear(); - disposeStringBinding(); - disposeNumberBinding(); - disposeBooleanBinding(); - disposeObjectBinding() - }); + it('get/setWeirdValues', () => { + setNumber('key_a', NaN, option); + assert.deepEqual(NaN, getNumber('key_a', option)); - it('get/setString', () => { - setString('key_a', 'hello', option); - setString('key_b', 'there', option); - assert.equal('hello', getString('key_a', option)); - assert.equal('there', getString('key_b', option)); - assert.equal(null, getString('key_c', option)); - }); + setNumber('key_a', +Infinity, option); + assert.equal(+Infinity, getNumber('key_a', option)); - it('get/setNumber', () => { - setNumber('key_a', 12, option); - setNumber('key_b', 3.4, option); - assert.equal(12, getNumber('key_a', option)); - assert.equal(3.4, getNumber('key_b', option)); - assert.equal(null, getNumber('key_c', option)); - }); + setNumber('key_a', -Infinity, option); + assert.equal(-Infinity, getNumber('key_a', option)); - it('get/setObject', () => { - const obj = {'foo': 2.3, 'bar': 'barstr'}; - setObject('key_a', obj, option); - assert.deepEqual(obj, getObject('key_a', option)); - }); + setNumber('key_a', 1 / 3, option); + assert.equal(1 / 3, getNumber('key_a', option)); - it('get/setWeirdValues', () => { - setNumber('key_a', NaN, option); - assert.deepEqual(NaN, getNumber('key_a', option)); + setNumber('key_a', -0, option); + assert.equal(-0, getNumber('key_a', option)); + }); - setNumber('key_a', +Infinity, option); - assert.equal(+Infinity, getNumber('key_a', option)); + it('set/getTab', () => { + setString(TAB, 'scalars', option); + assert.equal('scalars', getString(TAB, option)); + }); - setNumber('key_a', -Infinity, option); - assert.equal(-Infinity, getNumber('key_a', option)); + describe('getInitializer', () => { + [ + {useLocalStorage: true, name: 'local storage', eventName: 'storage'}, + {useLocalStorage: false, name: 'hash storage', eventName: 'hashchange'}, + ].forEach(({useLocalStorage, name, eventName}) => { + describe(name, () => { + const options = { + useLocalStorage, + defaultValue: 'baz', + polymerProperty: 'prop', + }; + + function setValue(key: string, value: string): void { + if (useLocalStorage) window.localStorage.setItem(key, value); + else setHash(`${key}=${value}`); + } + + it('sets the polymerProperty with the value', () => { + setValue('foo', 'bar'); + const initializer = getStringInitializer('foo', options); + const fakeScope = {prop: null}; + initializer.call(fakeScope); + assert.equal(fakeScope.prop, 'bar'); + }); - setNumber('key_a', 1 / 3, option); - assert.equal(1 / 3, getNumber('key_a', option)); + it('sets the prop with defaultValue when value is missing', () => { + const initializer = getStringInitializer('foo', options); + const fakeScope = {prop: null}; + initializer.call(fakeScope); + assert.equal(fakeScope.prop, 'baz'); + }); - setNumber('key_a', -0, option); - assert.equal(-0, getNumber('key_a', option)); - }); + it(`reacts to '${eventName}' and sets the new value (simulated)`, () => { + setValue('foo', ''); - it('set/getTab', () => { - setString(TAB, 'scalars', option); - assert.equal('scalars', getString(TAB, option)); - }); + const initializer = getStringInitializer('foo', options); + const fakeScope = {prop: null}; + initializer.call(fakeScope); - describe('getInitializer', () => { - [ - {useLocalStorage: true, name: 'local storage', eventName: 'storage'}, - {useLocalStorage: false, name: 'hash storage', eventName: 'hashchange'} - ].forEach(({useLocalStorage, name, eventName}) => { - describe(name, () => { - const options = { - useLocalStorage, - defaultValue: 'baz', - polymerProperty: 'prop', - }; - - function setValue(key: string, value: string): void { - if (useLocalStorage) window.localStorage.setItem(key, value); - else setHash(`${key}=${value}`); - } - - it('sets the polymerProperty with the value', () => { - setValue('foo', 'bar'); - const initializer = getStringInitializer('foo', options); - const fakeScope = {prop: null}; - initializer.call(fakeScope); - assert.equal(fakeScope.prop, 'bar'); - }); + // Simulate the hashchange. + setValue('foo', 'changed'); + window.dispatchEvent(new Event(eventName)); - it('sets the prop with defaultValue when value is missing', () => { - const initializer = getStringInitializer('foo', options); - const fakeScope = {prop: null}; - initializer.call(fakeScope); - assert.equal(fakeScope.prop, 'baz'); - }); + assert.equal(fakeScope.prop, 'changed'); + }); - it(`reacts to '${eventName}' and sets the new value (simulated)`, () => { - setValue('foo', ''); + // It is hard to test against real URL hash and we use fakeHash for + // testing and fakeHash does not emit any event for a change. + if (useLocalStorage) { + it(`reacts to change and sets the new value (real)`, () => { + setString('foo', '', options); - const initializer = getStringInitializer('foo', options); - const fakeScope = {prop: null}; - initializer.call(fakeScope); + const initializer = getStringInitializer('foo', options); + const fakeScope1 = {prop: null}; + initializer.call(fakeScope1); + const fakeScope2 = {prop: 'bar'}; + initializer.call(fakeScope2); - // Simulate the hashchange. - setValue('foo', 'changed'); - window.dispatchEvent(new Event(eventName)); + setString('foo', 'changed', options); - assert.equal(fakeScope.prop, 'changed'); + assert.equal(fakeScope1.prop, 'changed'); + assert.equal(fakeScope2.prop, 'changed'); + }); + } }); - - // It is hard to test against real URL hash and we use fakeHash for - // testing and fakeHash does not emit any event for a change. - if (useLocalStorage) { - it(`reacts to change and sets the new value (real)`, () => { - setString('foo', '', options); - - const initializer = getStringInitializer('foo', options); - const fakeScope1 = {prop: null}; - initializer.call(fakeScope1); - const fakeScope2 = {prop: 'bar'}; - initializer.call(fakeScope2); - - setString('foo', 'changed', options); - - assert.equal(fakeScope1.prop, 'changed'); - assert.equal(fakeScope2.prop, 'changed'); - }); - } }); }); - }); - describe('advanced setter', () => { - const keyName = 'key'; + describe('advanced setter', () => { + const keyName = 'key'; - beforeEach(() => { - assert.isFalse(getHash().includes(keyName)); - }); + beforeEach(() => { + assert.isFalse(getHash().includes(keyName)); + }); - it('sets url hash', () => { - setNumber(keyName, 1, option); - assert.isTrue(getHash().includes(keyName)); - }); + it('sets url hash', () => { + setNumber(keyName, 1, option); + assert.isTrue(getHash().includes(keyName)); + }); - it('unsets url hash when value equals defaultValue', () => { - setNumber(keyName, 1, Object.assign({}, option, {defaultValue: 0})); - assert.isTrue(getHash().includes(keyName)); + it('unsets url hash when value equals defaultValue', () => { + setNumber(keyName, 1, Object.assign({}, option, {defaultValue: 0})); + assert.isTrue(getHash().includes(keyName)); - // If previous value on hash (which is 1 from above) matches the new value - // it does not unset the url value. - setNumber(keyName, 1, Object.assign({}, option, {defaultValue: 2})); - assert.isTrue(getHash().includes(keyName)); + // If previous value on hash (which is 1 from above) matches the new value + // it does not unset the url value. + setNumber(keyName, 1, Object.assign({}, option, {defaultValue: 2})); + assert.isTrue(getHash().includes(keyName)); - setNumber(keyName, 2, Object.assign({}, option, {defaultValue: 2})); - assert.isFalse(getHash().includes(keyName)); + setNumber(keyName, 2, Object.assign({}, option, {defaultValue: 2})); + assert.isFalse(getHash().includes(keyName)); + }); }); }); -}); - -} // namespace tf_storage +} // namespace tf_storage diff --git a/tensorboard/components/tf_storage/test/tests.html b/tensorboard/components/tf_storage/test/tests.html index 77a4f6f102..4a1d5f06ac 100644 --- a/tensorboard/components/tf_storage/test/tests.html +++ b/tensorboard/components/tf_storage/test/tests.html @@ -1,4 +1,4 @@ - + - + - - + + - + + diff --git a/tensorboard/components/tf_storage/tf-storage.html b/tensorboard/components/tf_storage/tf-storage.html index ea9b1db572..a011ce7315 100644 --- a/tensorboard/components/tf_storage/tf-storage.html +++ b/tensorboard/components/tf_storage/tf-storage.html @@ -15,8 +15,8 @@ limitations under the License. --> - - + + diff --git a/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts b/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts index 57c78313d8..1dc8e480f3 100644 --- a/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts +++ b/tensorboard/components/tf_tensorboard/autoReloadBehavior.ts @@ -13,56 +13,60 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_tensorboard { + export var AUTORELOAD_LOCALSTORAGE_KEY = 'TF.TensorBoard.autoReloadEnabled'; -export var AUTORELOAD_LOCALSTORAGE_KEY = 'TF.TensorBoard.autoReloadEnabled'; + var getAutoReloadFromLocalStorage: () => boolean = () => { + var val = window.localStorage.getItem(AUTORELOAD_LOCALSTORAGE_KEY); + return val === 'true' || val == null; // defaults to true + }; -var getAutoReloadFromLocalStorage: () => boolean = () => { - var val = window.localStorage.getItem(AUTORELOAD_LOCALSTORAGE_KEY); - return val === 'true' || val == null; // defaults to true -}; - -function forceDisableAutoReload(): boolean { - return new URLSearchParams(window.location.search).has('_DisableAutoReload'); -} + function forceDisableAutoReload(): boolean { + return new URLSearchParams(window.location.search).has( + '_DisableAutoReload' + ); + } -/** - * @polymerBehavior - */ -export var AutoReloadBehavior = { - properties: { - autoReloadEnabled: { - type: Boolean, - observer: '_autoReloadObserver', - value: getAutoReloadFromLocalStorage, + /** + * @polymerBehavior + */ + export var AutoReloadBehavior = { + properties: { + autoReloadEnabled: { + type: Boolean, + observer: '_autoReloadObserver', + value: getAutoReloadFromLocalStorage, + }, + _autoReloadId: { + type: Number, + }, + autoReloadIntervalSecs: { + type: Number, + value: 30, + }, }, - _autoReloadId: { - type: Number, + detached: function() { + window.clearTimeout(this._autoReloadId); }, - autoReloadIntervalSecs: { - type: Number, - value: 30, + _autoReloadObserver: function(autoReload) { + window.localStorage.setItem(AUTORELOAD_LOCALSTORAGE_KEY, autoReload); + if (autoReload && !forceDisableAutoReload()) { + this._autoReloadId = window.setTimeout( + () => this._doAutoReload(), + this.autoReloadIntervalSecs * 1000 + ); + } else { + window.clearTimeout(this._autoReloadId); + } }, - }, - detached: function() { - window.clearTimeout(this._autoReloadId); - }, - _autoReloadObserver: function(autoReload) { - window.localStorage.setItem(AUTORELOAD_LOCALSTORAGE_KEY, autoReload); - if (autoReload && !forceDisableAutoReload()) { + _doAutoReload: function() { + if (this.reload == null) { + throw new Error('AutoReloadBehavior requires a reload method'); + } + this.reload(); this._autoReloadId = window.setTimeout( - () => this._doAutoReload(), this.autoReloadIntervalSecs * 1000); - } else { - window.clearTimeout(this._autoReloadId); - } - }, - _doAutoReload: function() { - if (this.reload == null) { - throw new Error('AutoReloadBehavior requires a reload method'); - } - this.reload(); - this._autoReloadId = window.setTimeout( - () => this._doAutoReload(), this.autoReloadIntervalSecs * 1000); - } -}; - -} // namespace tf_tensorboard + () => this._doAutoReload(), + this.autoReloadIntervalSecs * 1000 + ); + }, + }; +} // namespace tf_tensorboard diff --git a/tensorboard/components/tf_tensorboard/default-plugins.html b/tensorboard/components/tf_tensorboard/default-plugins.html index 7f8de9f046..307b65ac0d 100644 --- a/tensorboard/components/tf_tensorboard/default-plugins.html +++ b/tensorboard/components/tf_tensorboard/default-plugins.html @@ -21,18 +21,30 @@ Each dashboard registers itself onto the global CustomElementRegistry, and is later instantiated dynamically with `document.createElement`. --> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/tensorboard/components/tf_tensorboard/plugin-dialog.html b/tensorboard/components/tf_tensorboard/plugin-dialog.html index 3d4bd5340d..a44f7c011e 100644 --- a/tensorboard/components/tf_tensorboard/plugin-dialog.html +++ b/tensorboard/components/tf_tensorboard/plugin-dialog.html @@ -15,9 +15,8 @@ limitations under the License. --> - - - + + diff --git a/tensorboard/components/tf_tensorboard/registry.ts b/tensorboard/components/tf_tensorboard/registry.ts index 73f05a08c2..cdc74c9588 100644 --- a/tensorboard/components/tf_tensorboard/registry.ts +++ b/tensorboard/components/tf_tensorboard/registry.ts @@ -13,93 +13,90 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace tf_tensorboard { + export enum ActiveDashboardsLoadState { + NOT_LOADED = 'NOT_LOADED', + LOADED = 'LOADED', + FAILED = 'FAILED', + } -export enum ActiveDashboardsLoadState { - NOT_LOADED = 'NOT_LOADED', - LOADED = 'LOADED', - FAILED = 'FAILED', -} + /** Registration for a plugin dashboard UI. */ + export interface Dashboard { + /** + * Name of the element for the dashboard (excluding the angle brackets on + * either side). For instance, tf-scalar-dashboard. Used to select the + * correct dashboard when a user enters it. + */ + elementName: string; -/** Registration for a plugin dashboard UI. */ -export interface Dashboard { + /** + * The name of the plugin associated with this dashboard. This string must + * match the PLUGIN_NAME specified by the backend of the plugin. Each plugin + * can be associated with no more than 1 plugin - this also means that each + * dashboard has a unique plugin field. + */ + plugin: string; - /** - * Name of the element for the dashboard (excluding the angle brackets on - * either side). For instance, tf-scalar-dashboard. Used to select the - * correct dashboard when a user enters it. - */ - elementName: string; + /** + * The string to show in the menu item for this dashboard within the + * navigation bar. That tab name may differ from the plugin name. For + * instance, the tab name should not use underscores to separate words. + */ + tabName: string; - /** - * The name of the plugin associated with this dashboard. This string must - * match the PLUGIN_NAME specified by the backend of the plugin. Each plugin - * can be associated with no more than 1 plugin - this also means that each - * dashboard has a unique plugin field. - */ - plugin: string; + /** + * Whether or not tf-tensorboard reload functionality should be disabled. + * + * If true, then: 1) the reload button in the top right corner of the + * TensorBoard UI will be shaded out; and 2) the timer that triggers the + * reload() method to be called every few seconds will be disabled. + */ + isReloadDisabled: boolean; - /** - * The string to show in the menu item for this dashboard within the - * navigation bar. That tab name may differ from the plugin name. For - * instance, the tab name should not use underscores to separate words. - */ - tabName: string; + /** + * Whether or not plugin DOM should be removed when navigated away. + * + * This allows the Polymer 'detached' event to happen. + */ + shouldRemoveDom: boolean; + } + + /** Typedef mapping plugin names to Dashboard registrations. */ + export type DashboardRegistry = {[key: string]: Dashboard}; /** - * Whether or not tf-tensorboard reload functionality should be disabled. + * Map of all registered dashboards. * - * If true, then: 1) the reload button in the top right corner of the - * TensorBoard UI will be shaded out; and 2) the timer that triggers the - * reload() method to be called every few seconds will be disabled. + * This object should only be mutated by the registerDashboard() function. */ - isReloadDisabled: boolean; + export let dashboardRegistry: DashboardRegistry = {}; /** - * Whether or not plugin DOM should be removed when navigated away. + * For legacy plugins, registers Dashboard for plugin into TensorBoard frontend. + * + * New plugins should implement the `frontend_metadata` method on the + * corresponding Python plugin to provide this information instead. * - * This allows the Polymer 'detached' event to happen. + * For legacy plugins: + * + * This function should be called after the Polymer custom element is defined. + * It's what allows the tf-tensorboard component to dynamically load it as a + * tab in TensorBoard's GUI. + * + * `elementName` and `plugin` are mandatory. `tabName` defaults to `plugin`. */ - shouldRemoveDom: boolean; -} - -/** Typedef mapping plugin names to Dashboard registrations. */ -export type DashboardRegistry = {[key: string]: Dashboard}; - -/** - * Map of all registered dashboards. - * - * This object should only be mutated by the registerDashboard() function. - */ -export let dashboardRegistry : DashboardRegistry = {}; - -/** - * For legacy plugins, registers Dashboard for plugin into TensorBoard frontend. - * - * New plugins should implement the `frontend_metadata` method on the - * corresponding Python plugin to provide this information instead. - * - * For legacy plugins: - * - * This function should be called after the Polymer custom element is defined. - * It's what allows the tf-tensorboard component to dynamically load it as a - * tab in TensorBoard's GUI. - * - * `elementName` and `plugin` are mandatory. `tabName` defaults to `plugin`. - */ -export function registerDashboard(dashboard: Dashboard) { - if (!dashboard.plugin) { - throw new Error('Dashboard.plugin must be present'); + export function registerDashboard(dashboard: Dashboard) { + if (!dashboard.plugin) { + throw new Error('Dashboard.plugin must be present'); + } + if (!dashboard.elementName) { + throw new Error('Dashboard.elementName must be present'); + } + if (dashboard.plugin in dashboardRegistry) { + throw new Error(`Plugin already registered: ${dashboard.plugin}`); + } + if (!dashboard.tabName) { + dashboard.tabName = dashboard.plugin; + } + dashboardRegistry[dashboard.plugin] = dashboard; } - if (!dashboard.elementName) { - throw new Error('Dashboard.elementName must be present'); - } - if (dashboard.plugin in dashboardRegistry) { - throw new Error(`Plugin already registered: ${dashboard.plugin}`); - } - if (!dashboard.tabName) { - dashboard.tabName = dashboard.plugin; - } - dashboardRegistry[dashboard.plugin] = dashboard; -} - -} // namespace tf_tensorboard +} // namespace tf_tensorboard diff --git a/tensorboard/components/tf_tensorboard/style.html b/tensorboard/components/tf_tensorboard/style.html index 0b374b3eaa..6e6afec29b 100644 --- a/tensorboard/components/tf_tensorboard/style.html +++ b/tensorboard/components/tf_tensorboard/style.html @@ -15,7 +15,7 @@ limitations under the License. --> - + diff --git a/tensorboard/components/vz_chart_helpers/vz-chart-helpers.ts b/tensorboard/components/vz_chart_helpers/vz-chart-helpers.ts index 000166e13d..84656580f8 100644 --- a/tensorboard/components/vz_chart_helpers/vz-chart-helpers.ts +++ b/tensorboard/components/vz_chart_helpers/vz-chart-helpers.ts @@ -13,209 +13,210 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ namespace vz_chart_helpers { + export interface Datum { + wall_time: Date; + step: number; + } -export interface Datum { - wall_time: Date; - step: number; -} - -export interface Scalar { - scalar: number; - smoothed: number; -} - -export type ScalarDatum = Datum & Scalar; - -export type DataFn = (run: string, tag: string) => Promise>; - -export interface LineChartSymbol { - // A single unicode character string representing the symbol. Maybe a diamond - // unicode character for instance. - character: string; - // A special method used by Plottable to draw the symbol in the line chart. - method: (() => Plottable.SymbolFactories.SymbolFactory); -} - -/** - * A list of symbols that line charts can cycle through per data series. - */ -export const SYMBOLS_LIST: LineChartSymbol[] = [ - { - character: '\u25FC', - method: Plottable.SymbolFactories.square, - }, - { - character: '\u25c6', - method: Plottable.SymbolFactories.diamond, - }, - { - character: '\u25B2', - method: Plottable.SymbolFactories.triangle, - }, - { - character: '\u2605', - method: Plottable.SymbolFactories.star, - }, - { - character: '\u271a', - method: Plottable.SymbolFactories.cross, - }, -]; + export interface Scalar { + scalar: number; + smoothed: number; + } -/** X axis choices for TensorBoard charts. */ -export enum XType { + export type ScalarDatum = Datum & Scalar; - /** Linear scale using the "step" property of the datum. */ - STEP = 'step', + export type DataFn = (run: string, tag: string) => Promise>; - /** Temporal scale using the "wall_time" property of the datum. */ - RELATIVE = 'relative', + export interface LineChartSymbol { + // A single unicode character string representing the symbol. Maybe a diamond + // unicode character for instance. + character: string; + // A special method used by Plottable to draw the symbol in the line chart. + method: () => Plottable.SymbolFactories.SymbolFactory; + } /** - * Temporal scale using the "relative" property of the datum if it is present - * or calculating from "wall_time" if it isn't. + * A list of symbols that line charts can cycle through per data series. */ - WALL_TIME = 'wall_time', -} + export const SYMBOLS_LIST: LineChartSymbol[] = [ + { + character: '\u25FC', + method: Plottable.SymbolFactories.square, + }, + { + character: '\u25c6', + method: Plottable.SymbolFactories.diamond, + }, + { + character: '\u25B2', + method: Plottable.SymbolFactories.triangle, + }, + { + character: '\u2605', + method: Plottable.SymbolFactories.star, + }, + { + character: '\u271a', + method: Plottable.SymbolFactories.cross, + }, + ]; + + /** X axis choices for TensorBoard charts. */ + export enum XType { + /** Linear scale using the "step" property of the datum. */ + STEP = 'step', + + /** Temporal scale using the "wall_time" property of the datum. */ + RELATIVE = 'relative', + + /** + * Temporal scale using the "relative" property of the datum if it is present + * or calculating from "wall_time" if it isn't. + */ + WALL_TIME = 'wall_time', + } -export type SymbolFn = (series: string) => Plottable.SymbolFactory; + export type SymbolFn = (series: string) => Plottable.SymbolFactory; + + export let Y_TOOLTIP_FORMATTER_PRECISION = 4; + export let STEP_FORMATTER_PRECISION = 4; + export let Y_AXIS_FORMATTER_PRECISION = 3; + export let TOOLTIP_Y_PIXEL_OFFSET = 20; + export let TOOLTIP_CIRCLE_SIZE = 4; + export let NAN_SYMBOL_SIZE = 6; + + export interface Point { + x: number; // pixel space + y: number; // pixel space + datum: ScalarDatum; + dataset: Plottable.Dataset; + } -export let Y_TOOLTIP_FORMATTER_PRECISION = 4; -export let STEP_FORMATTER_PRECISION = 4; -export let Y_AXIS_FORMATTER_PRECISION = 3; -export let TOOLTIP_Y_PIXEL_OFFSET = 20; -export let TOOLTIP_CIRCLE_SIZE = 4; -export let NAN_SYMBOL_SIZE = 6; + export interface TooltipColumnState { + smoothingEnabled: boolean; + } -export interface Point { - x: number; // pixel space - y: number; // pixel space - datum: ScalarDatum; - dataset: Plottable.Dataset; -} + export interface TooltipColumn { + title: string; + static: boolean; + evaluate: (p: Point, status?: TooltipColumnState) => string; + } -export interface TooltipColumnState { - smoothingEnabled: boolean; -} + /* Create a formatter function that will switch between exponential and + * regular display depending on the scale of the number being formatted, + * and show `digits` significant digits. + */ + export function multiscaleFormatter(digits: number): (v: number) => string { + return (v: number) => { + let absv = Math.abs(v); + if (absv < 1e-15) { + // Sometimes zero-like values get an annoying representation + absv = 0; + } + let f: (x: number) => string; + if (absv >= 1e4) { + f = d3.format('.' + digits + '~e'); + } else if (absv > 0 && absv < 0.01) { + f = d3.format('.' + digits + '~e'); + } else { + f = d3.format('.' + digits + '~g'); + } + return f(v); + }; + } -export interface TooltipColumn { - title: string; - static: boolean; - evaluate: ((p: Point, status?: TooltipColumnState) => string); -} + /* Compute an appropriate domain given an array of all the values that are + * going to be displayed. If ignoreOutliers is true, it will ignore the + * lowest 10% and highest 10% of the data when computing a domain. + * It has n log n performance when ignoreOutliers is true, as it needs to + * sort the data. + */ + export function computeDomain(values: number[], ignoreOutliers: boolean) { + // Don't include infinities and NaNs in the domain computation. + values = values.filter((z) => isFinite(z)); -/* Create a formatter function that will switch between exponential and - * regular display depending on the scale of the number being formatted, - * and show `digits` significant digits. - */ -export function multiscaleFormatter(digits: number): ((v: number) => string) { - return (v: number) => { - let absv = Math.abs(v); - if (absv < 1E-15) { - // Sometimes zero-like values get an annoying representation - absv = 0; + if (values.length === 0) { + return [-0.1, 1.1]; } - let f: (x: number) => string; - if (absv >= 1E4) { - f = d3.format('.' + digits + '~e'); - } else if (absv > 0 && absv < 0.01) { - f = d3.format('.' + digits + '~e'); + let a: number; + let b: number; + if (ignoreOutliers) { + let sorted = _.sortBy(values); + a = d3.quantile(sorted, 0.05); + b = d3.quantile(sorted, 0.95); } else { - f = d3.format('.' + digits + '~g'); + a = d3.min(values); + b = d3.max(values); } - return f(v); - }; -} -/* Compute an appropriate domain given an array of all the values that are - * going to be displayed. If ignoreOutliers is true, it will ignore the - * lowest 10% and highest 10% of the data when computing a domain. - * It has n log n performance when ignoreOutliers is true, as it needs to - * sort the data. - */ -export function computeDomain(values: number[], ignoreOutliers: boolean) { - // Don't include infinities and NaNs in the domain computation. - values = values.filter(z => isFinite(z)); + let padding: number; + let span = b - a; + if (span === 0) { + // If b===a, we would create an empty range. We instead select the range + // [0, 2*a] if a > 0, or [-2*a, 0] if a < 0, plus a little bit of + // extra padding on the top and bottom of the plot. + padding = Math.abs(a) * 1.1 + 1.1; + } else { + padding = span * 0.2; + } - if (values.length === 0) { - return [-0.1, 1.1]; - } - let a: number; - let b: number; - if (ignoreOutliers) { - let sorted = _.sortBy(values); - a = d3.quantile(sorted, 0.05); - b = d3.quantile(sorted, 0.95); - } else { - a = d3.min(values); - b = d3.max(values); - } + let lower: number; + if (a >= 0 && a < span) { + // We include the intercept (y = 0) if doing so less than doubles the span + // of the y-axis. (We actually select a lower bound that's slightly less + // than 0 so that 0.00 will clearly be written on the lower edge of the + // chart. The label on the lowest tick is often filtered out.) + lower = -0.1 * b; + } else { + lower = a - padding; + } - let padding: number; - let span = b - a; - if (span === 0) { - // If b===a, we would create an empty range. We instead select the range - // [0, 2*a] if a > 0, or [-2*a, 0] if a < 0, plus a little bit of - // extra padding on the top and bottom of the plot. - padding = Math.abs(a) * 1.1 + 1.1; - } else { - padding = span * 0.2; + let domain = [lower, b + padding]; + domain = d3 + .scaleLinear() + .domain(domain) + .nice() + .domain(); + return domain; } - let lower: number; - if (a >= 0 && a < span) { - // We include the intercept (y = 0) if doing so less than doubles the span - // of the y-axis. (We actually select a lower bound that's slightly less - // than 0 so that 0.00 will clearly be written on the lower edge of the - // chart. The label on the lowest tick is often filtered out.) - lower = -0.1 * b; - } else { - lower = a - padding; + export function accessorize(key: string): Plottable.IAccessor { + // tslint:disable-next-line:no-any be quiet tsc + return (d: any, index: number, dataset: Plottable.Dataset) => d[key]; } + export interface XComponents { + /* tslint:disable */ + scale: Plottable.Scales.Linear | Plottable.Scales.Time; + axis: Plottable.Axes.Numeric | Plottable.Axes.Time; + accessor: Plottable.IAccessor; + /* tslint:enable */ + } - let domain = [lower, b + padding]; - domain = d3.scaleLinear().domain(domain).nice().domain(); - return domain; -} - -export function accessorize(key: string): Plottable.IAccessor { - // tslint:disable-next-line:no-any be quiet tsc - return (d: any, index: number, dataset: Plottable.Dataset) => d[key]; -} - -export interface XComponents { - /* tslint:disable */ - scale: Plottable.Scales.Linear|Plottable.Scales.Time, - axis: Plottable.Axes.Numeric|Plottable.Axes.Time, - accessor: Plottable.IAccessor, - /* tslint:enable */ -} - -export const stepFormatter = d3.format(`.${STEP_FORMATTER_PRECISION}~s`); -export function stepX(): XComponents { - let scale = new Plottable.Scales.Linear(); - scale.tickGenerator(Plottable.Scales.TickGenerators.integerTickGenerator()); - let axis = new Plottable.Axes.Numeric(scale, 'bottom'); - axis.formatter(stepFormatter); - return { - scale: scale, - axis: axis, - accessor: (d: Datum) => d.step, - }; -} + export const stepFormatter = d3.format(`.${STEP_FORMATTER_PRECISION}~s`); + export function stepX(): XComponents { + let scale = new Plottable.Scales.Linear(); + scale.tickGenerator(Plottable.Scales.TickGenerators.integerTickGenerator()); + let axis = new Plottable.Axes.Numeric(scale, 'bottom'); + axis.formatter(stepFormatter); + return { + scale: scale, + axis: axis, + accessor: (d: Datum) => d.step, + }; + } -export let timeFormatter = Plottable.Formatters.time('%a %b %e, %H:%M:%S'); + export let timeFormatter = Plottable.Formatters.time('%a %b %e, %H:%M:%S'); -export function wallX(): XComponents { - let scale = new Plottable.Scales.Time(); - return { - scale: scale, - axis: new Plottable.Axes.Time(scale, 'bottom'), - accessor: (d: Datum) => d.wall_time, - }; -} -export let relativeAccessor = + export function wallX(): XComponents { + let scale = new Plottable.Scales.Time(); + return { + scale: scale, + axis: new Plottable.Axes.Time(scale, 'bottom'), + accessor: (d: Datum) => d.wall_time, + }; + } + export let relativeAccessor = // tslint:disable-next-line:no-any be quiet tsc (d: any, index: number, dataset: Plottable.Dataset) => { // We may be rendering the final-point datum for scatterplot. @@ -228,53 +229,52 @@ export let relativeAccessor = // empty (after all, it iterates over the data), but lets guard just // to be safe. let first = data.length > 0 ? +data[0].wall_time : 0; - return (+d.wall_time - first) / (60 * 60 * 1000); // ms to hours + return (+d.wall_time - first) / (60 * 60 * 1000); // ms to hours }; -export let relativeFormatter = (n: number) => { - // we will always show 2 units of precision, e.g days and hours, or - // minutes and seconds, but not hours and minutes and seconds - let ret = ''; - let days = Math.floor(n / 24); - n -= (days * 24); - if (days) { - ret += days + 'd '; - } - let hours = Math.floor(n); - n -= hours; - n *= 60; - if (hours || days) { - ret += hours + 'h '; - } - let minutes = Math.floor(n); - n -= minutes; - n *= 60; - if (minutes || hours || days) { - ret += minutes + 'm '; - } - let seconds = Math.floor(n); - return ret + seconds + 's'; -}; -export function relativeX(): XComponents { - let scale = new Plottable.Scales.Linear(); - return { - scale: scale, - axis: new Plottable.Axes.Numeric(scale, 'bottom'), - accessor: relativeAccessor, + export let relativeFormatter = (n: number) => { + // we will always show 2 units of precision, e.g days and hours, or + // minutes and seconds, but not hours and minutes and seconds + let ret = ''; + let days = Math.floor(n / 24); + n -= days * 24; + if (days) { + ret += days + 'd '; + } + let hours = Math.floor(n); + n -= hours; + n *= 60; + if (hours || days) { + ret += hours + 'h '; + } + let minutes = Math.floor(n); + n -= minutes; + n *= 60; + if (minutes || hours || days) { + ret += minutes + 'm '; + } + let seconds = Math.floor(n); + return ret + seconds + 's'; }; -} - -export function getXComponents(xType: string): XComponents { - switch (xType) { - case XType.STEP: - return stepX(); - case XType.WALL_TIME: - return wallX(); - case XType.RELATIVE: - return relativeX(); - default: - throw new Error('invalid xType: ' + xType); + export function relativeX(): XComponents { + let scale = new Plottable.Scales.Linear(); + return { + scale: scale, + axis: new Plottable.Axes.Numeric(scale, 'bottom'), + accessor: relativeAccessor, + }; } -} -} // namespace vz_chart_helpers + export function getXComponents(xType: string): XComponents { + switch (xType) { + case XType.STEP: + return stepX(); + case XType.WALL_TIME: + return wallX(); + case XType.RELATIVE: + return relativeX(); + default: + throw new Error('invalid xType: ' + xType); + } + } +} // namespace vz_chart_helpers diff --git a/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html b/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html index d12a889ecf..f501955120 100644 --- a/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html +++ b/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html @@ -15,8 +15,8 @@ limitations under the License. --> - - + + - - + + vz-example-viewer demo - - - + + +
@@ -386,65 +394,121 @@
- -