Skip to content
80 changes: 47 additions & 33 deletions tensorboard/components/tf_backend/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export interface Router {
environment: () => string;
experiments: () => string;
isDemoMode: () => boolean;
pluginRoute: (pluginName: string, route: string) => string;
pluginRoute: (pluginName: string, route: string,
params?: URLSearchParams, demoCustomExt?: string) => string;
pluginsListing: () => string;
runs: () => string;
runsForExperiment: (id: tf_backend.ExperimentId) => string;
Expand All @@ -28,33 +29,33 @@ export interface Router {
* Create a router for communicating with the TensorBoard backend. You
* can pass this to `setRouter` to make it the global router.
*
* @param pathPrefix {string=} The base prefix for data endpoints.
* @param dataDir {string=} The base prefix for data endpoints.
* @param demoMode {boolean=} Whether to modify urls for filesystem demo usage.
*/
export function createRouter(pathPrefix = 'data', demoMode = false): Router {
if (pathPrefix[pathPrefix.length - 1] === '/') {
pathPrefix = pathPrefix.slice(0, pathPrefix.length - 1);
export function createRouter(dataDir = 'data', demoMode = false): Router {
if (dataDir[dataDir.length - 1] === '/') {
dataDir = dataDir.slice(0, dataDir.length - 1);
}
const createPath = demoMode ? createDemoPath : createProdPath;
const createDataPath = demoMode ? createDemoDataPath : createProdDataPath;
const ext = demoMode ? '.json' : '';
return {
environment: () => createPath(pathPrefix, '/environment', ext),
experiments: () => createPath(pathPrefix, '/experiments', ext),
environment: () => createDataPath(dataDir, '/environment', ext),
experiments: () => createDataPath(dataDir, '/experiments', ext),
isDemoMode: () => demoMode,
pluginRoute: (pluginName: string, route: string,
params?: URLSearchParams, demoCustomExt = ext): string => {

return createPath(
demoMode ? pathPrefix : pathPrefix + '/plugin',
return createDataPath(
demoMode ? dataDir : dataDir + '/plugin',
`/${pluginName}${route}`,
demoCustomExt,
params);
},
pluginsListing: () => createPath(pathPrefix, '/plugins_listing', ext),
runs: () => createPath(pathPrefix, '/runs', ext),
pluginsListing: () => createDataPath(dataDir, '/plugins_listing', ext),
runs: () => createDataPath(dataDir, '/runs', ext),
runsForExperiment: id => {
return createPath(
pathPrefix,
return createDataPath(
dataDir,
'/experiment_runs',
ext,
createSearchParam({experiment: String(id)}));
Expand Down Expand Up @@ -86,42 +87,55 @@ 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);
// Use URL to normalize pathPrefix with leading slash and without.
url.pathname = pathPrefix + path;
if (params) url.search = params.toString();
return url.pathname + url.search;
function createProdDataPath(dataDir: string, route: string,
ext: string, params: URLSearchParams = new URLSearchParams()): string {
let relativePath = dataDir + route;
if (String(params)) {
const delimiter = route.includes('?') ? '&' : '?';
relativePath += delimiter + String(params);
}
return relativePath;
}

/**
* Creates a URL for demo.
* Creates a URL for demo apps.
*
* [1]: Demo pages are served as files and data routes are served as JSON files.
* For shareability and ease of use, the data files are served at root[2], "/",
* thus, the demo data path should return the absolute path regardless of
* current pathname.
*
* [2]: See the path property of tensorboard/demo/BUILD:demo_data.
*
* e.g.,
* > createDemoPath('a', '/b', '.json', {a: 1})
* > createDemoDataPath('a', '/b', '.json', {a: 1})
* < '/a/b_a_1.json'
*/
function createDemoPath(pathPrefix: string, path: string,
ext: string, params?: URLSearchParams): string {
function createDemoDataPath(dataDir: string, route: string,
ext: string, params: URLSearchParams = new 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, '_') : '';
const absRoute = route.startsWith('/') ? route : '/' + route;
const absUrl = new URL(route, window.location.href);
let {pathname: normalizedPath, searchParams: normalizedSearchParams} = absUrl;
const queryParam = [normalizedSearchParams, params]
.map(p => String(p))
.filter(Boolean)
.join('&');
const encodedQueryParam = queryParam.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;
const pathname = `${dataDir}/${normalizedPath}${ext}`;
// See [1] for the reason why we are forming an absolute path here.
const absPathname = pathname.startsWith('/') ? pathname : '/' + pathname;
return absPathname;
}

export function createSearchParam(params: QueryParams = {}): URLSearchParams {
Expand Down
Loading