diff --git a/tensorboard/components/tf_backend/environmentStore.ts b/tensorboard/components/tf_backend/environmentStore.ts index caf219241a..6798df5772 100644 --- a/tensorboard/components/tf_backend/environmentStore.ts +++ b/tensorboard/components/tf_backend/environmentStore.ts @@ -44,15 +44,15 @@ export class EnvironmentStore extends BaseStore { } public getDataLocation(): string { - return this.environment.dataLocation; + return this.environment ? this.environment.dataLocation : ''; } public getMode(): Mode { - return this.environment.mode; + return this.environment ? this.environment.mode : null; } public getWindowTitle(): string { - return this.environment.windowTitle; + return this.environment ? this.environment.windowTitle : ''; } } diff --git a/tensorboard/components/tf_backend/router.ts b/tensorboard/components/tf_backend/router.ts index a1a620b339..2f14631852 100644 --- a/tensorboard/components/tf_backend/router.ts +++ b/tensorboard/components/tf_backend/router.ts @@ -35,23 +35,30 @@ export function createRouter(dataDir = 'data', demoMode = false): Router { if (dataDir[dataDir.length - 1] === '/') { dataDir = dataDir.slice(0, dataDir.length - 1); } - function standardRoute(route: string, demoExtension = '.json'): - ((tag: string, run: string) => string) { - return function(tag: string, run: string): string { - return dataDir + '/' + addParams(route, {tag, run}); - }; - } - function pluginRoute(pluginName: string, route: string): string { - return `${dataDir}/plugin/${pluginName}${route}`; - } + const createPath = demoMode ? createDemoPath : createProdPath; + const ext = demoMode ? '.json' : ''; return { - environment: () => dataDir + '/environment', - experiments: () => dataDir + '/experiments', + environment: () => createPath(dataDir, '/environment', ext), + experiments: () => createPath(dataDir, '/experiments', ext), isDemoMode: () => demoMode, - pluginRoute, - pluginsListing: () => dataDir + '/plugins_listing', - runs: () => dataDir + '/runs' + (demoMode ? '.json' : ''), - runsForExperiment: id => `${dataDir}/experiment_runs?experiment=${id}`, + pluginRoute: (pluginName: string, route: string, + params?: URLSearchParams, demoCustomExt = ext): string => { + + return createPath( + demoMode ? dataDir : dataDir + '/plugin', + `/${pluginName}${route}`, + demoCustomExt, + params); + }, + pluginsListing: () => createPath(dataDir, '/plugins_listing', ext), + runs: () => createPath(dataDir, '/runs', ext), + runsForExperiment: id => { + return createPath( + dataDir, + '/experiment_runs', + ext, + createSearchParam({experiment: String(id)})); + }, }; }; @@ -79,4 +86,52 @@ export function setRouter(router: Router): void { _router = router; } +function createProdPath(pathPrefix: string, path: string, + ext: string, params?: URLSearchParams): string { + + const url = new URL(`${window.location.origin}/${pathPrefix}${path}`); + if (params) url.search = params.toString(); + return url.pathname + url.search; +} + +/** + * Creates a URL for demo. + * e.g., + * > createDemoPath('a', '/b', '.json', {a: 1}) + * < '/a/b_a_1.json' + */ +function createDemoPath(pathPrefix: string, path: string, + ext: string, params?: URLSearchParams): string { + + // First, parse the path in a safe manner by constructing a URL. We don't + // trust the path supplied by consumer. + const prefixLessUrl = new URL(`${window.location.origin}/${path}`); + let {pathname: normalizedPath} = prefixLessUrl; + const encodedQueryParam = params ? + params.toString().replace(/[&=%]/g, '_') : ''; + + // Strip leading slashes. + normalizedPath = normalizedPath.replace(/^\/+/g, ''); + // Convert slashes to underscores. + normalizedPath = normalizedPath.replace(/\//g, '_'); + // Add query parameter as path if it is present. + if (encodedQueryParam) normalizedPath += `_${encodedQueryParam}`; + const url = new URL(`${window.location.origin}`); + + // All demo data are serialized in JSON format. + url.pathname = `${pathPrefix}/${normalizedPath}${ext}`; + return url.pathname + url.search; +} + +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/test/backendTests.ts b/tensorboard/components/tf_backend/test/backendTests.ts index a4ed736afb..52652f2567 100644 --- a/tensorboard/components/tf_backend/test/backendTests.ts +++ b/tensorboard/components/tf_backend/test/backendTests.ts @@ -14,6 +14,8 @@ limitations under the License. ==============================================================================*/ namespace tf_backend { +const {assert} = chai; + describe('urlPathHelpers', () => { it('addParams leaves input untouched when there are no parameters', () => { const actual = addParams('http://foo', {a: undefined, b: undefined}); @@ -45,16 +47,12 @@ describe('backend tests', () => { let rm: RequestManager; const base = 'data'; const demoRouter = createRouter(base, /*demoMode=*/true); + beforeEach(() => { setRouter(demoRouter); rm = new RequestManager(); }); - it('trailing slash removed from base route', () => { - const r = createRouter('foo/'); - chai.assert.equal(r.runs(), 'foo/runs'); - }); - it('runToTag helpers work', () => { const r2t: RunToTag = { run1: ['foo', 'bar', 'zod'], @@ -76,6 +74,176 @@ describe('backend tests', () => { chai.assert.deepEqual(getRunsNamed(empty2), ['run1', 'run2']); chai.assert.deepEqual(getTags(empty2), []); }); + + describe('router', () => { + it('removes trailing slash from base route', () => { + const r = createRouter('foo/'); + assert.equal(r.runs(), '/foo/runs'); + }); + + describe('prod mode', () => { + beforeEach(function() { + this.router = createRouter(base, /*demoMode=*/false); + }); + + it('returns correct value for #environment', function() { + assert.equal(this.router.environment(), '/data/environment'); + }); + + it('returns correct value for #experiments', function() { + assert.equal(this.router.experiments(), '/data/experiments'); + }); + + it('returns correct value for #isDemoMode', function() { + assert.equal(this.router.isDemoMode(), false); + }); + + describe('#pluginRoute', () => { + it('encodes slash correctly', function() { + assert.equal( + this.router.pluginRoute('scalars', '/scalar'), + '/data/plugin/scalars/scalar'); + }); + + it('encodes query param correctly', function() { + assert.equal( + this.router.pluginRoute( + 'scalars', + '/a', + createSearchParam({b: 'c', d: ['1', '2']})), + '/data/plugin/scalars/a?b=c&d=1&d=2'); + }); + + it('encodes parenthesis correctly', function() { + assert.equal( + this.router.pluginRoute('scalars', '/a', + createSearchParam({foo: '()'})), + '/data/plugin/scalars/a?foo=%28%29'); + }); + + it('encodes query param the same as #addParams', function() { + assert.equal( + this.router.pluginRoute( + 'scalars', + '/a', + createSearchParam({b: 'c', d: ['1']})), + addParams('/data/plugin/scalars/a', {b: 'c', d: ['1']})); + assert.equal( + this.router.pluginRoute( + 'scalars', + '/a', + createSearchParam({foo: '()'})), + addParams('/data/plugin/scalars/a', {foo: '()'})); + }); + + it('ignores custom extension', function() { + assert.equal( + this.router.pluginRoute('scalars', '/a', undefined, 'meow'), + '/data/plugin/scalars/a'); + }); + }); + + it('returns correct value for #pluginsListing', function() { + assert.equal(this.router.pluginsListing(), '/data/plugins_listing'); + }); + + it('returns correct value for #runs', function() { + assert.equal(this.router.runs(), '/data/runs'); + }); + + it('returns correct value for #runsForExperiment', function() { + // No experiment id is passed. + assert.equal( + this.router.runsForExperiment(''), + '/data/experiment_runs'); + assert.equal( + this.router.runsForExperiment('1'), + '/data/experiment_runs?experiment=1'); + assert.equal( + this.router.runsForExperiment('1&foo=false'), + '/data/experiment_runs?experiment=1%26foo%3Dfalse'); + }); + }); + + describe('demoMode', () => { + beforeEach( function() { + this.router = createRouter(base, /*demoMode=*/true); + }); + + it('returns correct value for #environment', function() { + assert.equal(this.router.environment(), '/data/environment.json'); + }); + + it('returns correct value for #experiments', function() { + assert.equal(this.router.experiments(), '/data/experiments.json'); + }); + + it('returns correct value for #isDemoMode', function() { + assert.equal(this.router.isDemoMode(), true); + }); + + describe('#pluginRoute', () => { + it('encodes slash correctly', function() { + assert.equal( + this.router.pluginRoute('scalars', '/scalar'), + '/data/scalars_scalar.json'); + }); + + it('encodes query param correctly', function() { + assert.equal( + this.router.pluginRoute( + 'scalars', + '/a', + createSearchParam({b: 'c', d: ['1', '2']})), + '/data/scalars_a_b_c_d_1_d_2.json'); + }); + + it('encodes parenthesis correctly', function() { + assert.equal( + this.router.pluginRoute( + 'scalars', + '/a', + createSearchParam({foo: '()'})), + '/data/scalars_a_foo__28_29.json'); + }); + + it('uses custom extension if provided', function() { + assert.equal( + this.router.pluginRoute('scalars', '/a', undefined, ''), + '/data/scalars_a'); + assert.equal( + this.router.pluginRoute('scalars', '/a', undefined, '.meow'), + '/data/scalars_a.meow'); + assert.equal( + this.router.pluginRoute('scalars', '/a'), + '/data/scalars_a.json'); + }); + }); + + it('returns correct value for #pluginsListing', function() { + assert.equal( + this.router.pluginsListing(), + '/data/plugins_listing.json'); + }); + + it('returns correct value for #runs', function() { + assert.equal(this.router.runs(), '/data/runs.json'); + }); + + it('returns correct value for #runsForExperiment', function() { + // No experiment id is passed. + assert.equal( + this.router.runsForExperiment(''), + '/data/experiment_runs.json'); + assert.equal( + this.router.runsForExperiment('1'), + '/data/experiment_runs_experiment_1.json'); + assert.equal( + this.router.runsForExperiment('1&foo=false'), + '/data/experiment_runs_experiment_1_26foo_3Dfalse.json'); + }); + }); + }); }); } // namespace tf_backend diff --git a/tensorboard/components/tf_backend/urlPathHelpers.ts b/tensorboard/components/tf_backend/urlPathHelpers.ts index 3a1a11a7fa..e98ca4d672 100644 --- a/tensorboard/components/tf_backend/urlPathHelpers.ts +++ b/tensorboard/components/tf_backend/urlPathHelpers.ts @@ -21,6 +21,8 @@ namespace tf_backend { */ export type QueryValue = string | string[]; +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, @@ -31,10 +33,11 @@ export type QueryValue = string | string[]; * 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: {[param: string]: QueryValue}): string { +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?' diff --git a/tensorboard/demo/data/audio_run_run1_tag_au1_2Faudio_2F0.json b/tensorboard/demo/data/audio_run_run1_tag_au1_2Faudio_2F0.json deleted file mode 100644 index 7dfe32c711..0000000000 --- a/tensorboard/demo/data/audio_run_run1_tag_au1_2Faudio_2F0.json +++ /dev/null @@ -1 +0,0 @@ -[{"query": "index=0&tag=au1%2Faudio%2F0&run=run1", "step": 0, "wall_time": 1461795049.203407, "content_type": "audio/wav"}] \ No newline at end of file diff --git a/tensorboard/demo/data/audio_run_run2_tag_au2_2Faudio_2F0.json b/tensorboard/demo/data/audio_run_run2_tag_au2_2Faudio_2F0.json deleted file mode 100644 index 13f9c2de42..0000000000 --- a/tensorboard/demo/data/audio_run_run2_tag_au2_2Faudio_2F0.json +++ /dev/null @@ -1 +0,0 @@ -[{"query": "index=0&tag=au2%2Faudio%2F0&run=run2", "step": 0, "wall_time": 1461795049.212815, "content_type": "audio/wav"}] \ No newline at end of file diff --git a/tensorboard/demo/data/environment.json b/tensorboard/demo/data/environment.json new file mode 100644 index 0000000000..2db30348c5 --- /dev/null +++ b/tensorboard/demo/data/environment.json @@ -0,0 +1,5 @@ +{ + "data_location": "demo_dir", + "mode": "logdir", + "window_title": "TensorBoard demo" +} diff --git a/tensorboard/demo/data/experiments.json b/tensorboard/demo/data/experiments.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/tensorboard/demo/data/experiments.json @@ -0,0 +1 @@ +[] diff --git a/tensorboard/demo/data/graph_run_run1.pbtxt b/tensorboard/demo/data/graph_run_run1.pbtxt deleted file mode 100644 index 2a6af32840..0000000000 --- a/tensorboard/demo/data/graph_run_run1.pbtxt +++ /dev/null @@ -1,9 +0,0 @@ -node { - name: "a" - op: "matmul" -} -node { - name: "b" - op: "matmul" - input: "a:0" -} diff --git a/tensorboard/demo/data/logdir b/tensorboard/demo/data/logdir deleted file mode 100644 index b6362b45d7..0000000000 --- a/tensorboard/demo/data/logdir +++ /dev/null @@ -1 +0,0 @@ -{"logdir": "/foo/some/fake/logdir"} \ No newline at end of file diff --git a/tensorboard/demo/data/runs.json b/tensorboard/demo/data/runs.json index e090390542..a85f62e6d1 100644 --- a/tensorboard/demo/data/runs.json +++ b/tensorboard/demo/data/runs.json @@ -1 +1 @@ -{"run1": {"scalars": ["foo/sin", "foo/cos", "foo/square", "bar/square"], "run_metadata": [], "compressedHistograms": ["histo1"], "images": ["im1/image/0", "im2/image/0"], "histograms": ["histo1"], "graph": true, "audio": ["au1/audio/0"]}, "run2": {"scalars": ["foo/cos", "foo/square", "bar/square"], "run_metadata": [], "compressedHistograms": ["histo2", "histo1"], "images": ["im1/image/0"], "histograms": ["histo2", "histo1"], "graph": true, "audio": ["au2/audio/0"]}} \ No newline at end of file +["run1", "run2"] diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/BUILD b/tensorboard/plugins/audio/tf_audio_dashboard/BUILD index ac5a78f045..ab52d8ed33 100644 --- a/tensorboard/plugins/audio/tf_audio_dashboard/BUILD +++ b/tensorboard/plugins/audio/tf_audio_dashboard/BUILD @@ -8,7 +8,6 @@ tf_web_library( name = "tf_audio_dashboard", srcs = [ "tf-audio-dashboard.html", - "tf-audio-grid.html", "tf-audio-loader.html", ], path = "/tf-audio-dashboard", @@ -31,22 +30,3 @@ tf_web_library( "@org_polymer_paper_styles", ], ) - -tf_web_library( - name = "index", - srcs = [ - "demo/index.html", - "index.html", - ], - path = "/tf-audio-dashboard", - deps = [ - ":tf_audio_dashboard", - "//tensorboard/components/tf_backend", - "//tensorboard/components/tf_imports:d3", - "//tensorboard/components/tf_imports:webcomponentsjs", - "//tensorboard/demo:demo_data", - "@org_polymer_iron_component_page", - "@org_polymer_iron_demo_helpers", - "@org_polymer_paper_styles", - ], -) diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/BUILD b/tensorboard/plugins/audio/tf_audio_dashboard/demo/BUILD new file mode 100644 index 0000000000..37a73df0b7 --- /dev/null +++ b/tensorboard/plugins/audio/tf_audio_dashboard/demo/BUILD @@ -0,0 +1,21 @@ +package(default_visibility = ["//tensorboard:internal"]) + +load("//tensorboard/defs:web.bzl", "tf_web_library") + +licenses(["notice"]) # Apache 2.0 + +tf_web_library( + name = "demo", + srcs = ["index.html"], + path = "/tf-audio-dashboard/demo", + deps = [ + "//tensorboard/components/tf_backend", + "//tensorboard/components/tf_imports:polymer", + "//tensorboard/components/tf_imports:webcomponentsjs", + "//tensorboard/demo:demo_data", + "//tensorboard/plugins/audio/tf_audio_dashboard", + "//tensorboard/plugins/audio/tf_audio_dashboard/demo/data", + "@org_polymer_iron_demo_helpers", + "@org_polymer_paper_styles", + ], +) diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/BUILD b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/BUILD new file mode 100644 index 0000000000..b1749fda4c --- /dev/null +++ b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/BUILD @@ -0,0 +1,11 @@ +package(default_visibility = ["//tensorboard:internal"]) + +load("//tensorboard/defs:web.bzl", "tf_web_library") + +licenses(["notice"]) # Apache 2.0 + +tf_web_library( + name = "data", + srcs = glob(["*"]), + path = "/data", +) diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_audio_run_run1_tag_foo_2Fsine.json b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_audio_run_run1_tag_foo_2Fsine.json new file mode 100644 index 0000000000..98883d364f --- /dev/null +++ b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_audio_run_run1_tag_foo_2Fsine.json @@ -0,0 +1 @@ +[{"query": "index=0&tag=foo%2Fsine&run=run1", "step": 0, "wall_time": 123, "content_type": "audio/wav"}] diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_audio_run_run2_tag_bar_2Fsquare.json b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_audio_run_run2_tag_bar_2Fsquare.json new file mode 100644 index 0000000000..964da6dddb --- /dev/null +++ b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_audio_run_run2_tag_bar_2Fsquare.json @@ -0,0 +1 @@ +[{"query": "index=0&tag=bar%2Fsquare&run=run2", "step": 0, "wall_time": 123, "content_type": "audio/wav"}] diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_individualAudio_index_0_run_run1_tag_foo_2Fsine_ts_123.wav b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_individualAudio_index_0_run_run1_tag_foo_2Fsine_ts_123.wav new file mode 100644 index 0000000000..fa63ff75d5 Binary files /dev/null and b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_individualAudio_index_0_run_run1_tag_foo_2Fsine_ts_123.wav differ diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_tags.json b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_tags.json new file mode 100644 index 0000000000..011900e218 --- /dev/null +++ b/tensorboard/plugins/audio/tf_audio_dashboard/demo/data/audio_tags.json @@ -0,0 +1,16 @@ +{ + "run1": { + "foo/sine": { + "description": "", + "displayName": "foo_sine", + "samples": 1 + } + }, + "run2": { + "bar/square": { + "description": "", + "displayName": "bar_square", + "samples": 1 + } + } +} diff --git a/tensorboard/plugins/audio/tf_audio_dashboard/demo/index.html b/tensorboard/plugins/audio/tf_audio_dashboard/demo/index.html index 7682bab970..15311bfd3f 100644 --- a/tensorboard/plugins/audio/tf_audio_dashboard/demo/index.html +++ b/tensorboard/plugins/audio/tf_audio_dashboard/demo/index.html @@ -17,15 +17,18 @@ --> + + Audio Dashboard Demo diff --git a/tensorboard/plugins/scalar/tf_scalar_dashboard/tf-scalar-card.html b/tensorboard/plugins/scalar/tf_scalar_dashboard/tf-scalar-card.html index 6fb3f2a47d..24557535c7 100644 --- a/tensorboard/plugins/scalar/tf_scalar_dashboard/tf-scalar-card.html +++ b/tensorboard/plugins/scalar/tf_scalar_dashboard/tf-scalar-card.html @@ -240,13 +240,14 @@ type: Function, value: function() { return ({tag, run, experiment}) => { - return tf_backend.addParams( - tf_backend.getRouter().pluginRoute('scalars', '/scalars'), - { + return tf_backend.getRouter().pluginRoute( + 'scalars', + '/scalars', + new URLSearchParams({ tag, run, experiment: experiment ? experiment.id : '', - }); + })); } }, },