-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ng: support path_prefix at router level #4423
Conversation
A passing thought and question… One technique that I’ve used in the past is to simply inspect It seems to me like that would be a bit simpler. Is there a reason that Or, in other words—
—why does this have to change? |
There is no guarantee that application will only have "/" route and it is legal for users to start the app at "/foo/bar/experiment/123/" |
If I launch with Anwyay, what I’ve typically done for this is to include in each page’s <!-- /index.html -->
<body initial-root=".">
<h1>Home</h1>
</body> <!-- /about/roadmap/index.html -->
<body initial-root="../..">
<h1>Our roadmap</h1>
</body> Then, when But I recognize that this is probably a bit harder for us since we don’t |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR seems to break Time Series in standalone TB :/
The app 404s on a request to https://.../experiment/defaultExperimentId/data/plugin/timeseries/tags
because metrics_data_source uses a url starting with '/'. I wonder, should we patch TBHttpClient
to add the path_prefix whenever its methods are called with an absolute-looking url?
Re: stamping the initial HTML with path_prefix (or the relative path to it, as per wchargin)
I don't have a strong preference between that approach and the current PR. If we end up needing to switch to the stamped HTML approach for performance reasons later, most of this PR's frontend code doesn't have to change.
@@ -44,6 +45,7 @@ import {Navigation, Route} from '../types'; | |||
/** @typehack */ import * as _typeHackNgrxEffects from '@ngrx/effects/effects'; | |||
/** @typehack */ import * as _typeHackModels from '@ngrx/store/src/models'; | |||
/** @typehack */ import * as _typeHackStore from '@ngrx/store'; | |||
import {getPathPrefixConfig} from '../../core/store'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just ran a test sync and it looks like we'll need an explicit dep to get it to build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing that. I was going to run the sync test after LGTM but you beat me to it!
tensorboard/http_api.md
Outdated
"path_prefix": "/foo/bar" | ||
"window_title": "Custom Name", | ||
} | ||
|
||
## `data/environment` | ||
|
||
Returns environment in which the TensorBoard app is running. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we've copied the exp-agnostic fields from /data/environment into /data/config, the doc section for data/environment is stale.
In this PR, anything consuming window_title
is either
- getting it properly from the exp-agnostic endpoint
/data/config
- or is stuck on the legacy
/data/environment
, and needs to migrate so that we can make /data/environment exp-specific
When imagining the future TB that supports multiple experiments, I would assume the more appropriate place to get the data_location (and other experiment-specific info) is not /data/env
or /data/config
, but from one of the experiment-specific endpoints that already exist (/data/experiments
or /data/experiment_runs
or /data/runs
). In this scenario, we don't need both /data/config
AND /data/env
, but only 1 of them to serve exp-agnostic prefix_path, window_title, so we may consider dropping one of them.
Firstly, do you agree that we'd eventually need [/data/config OR /data/env], but not both?
If so, it sounds like the progression after this PR would be to:
- migrate consumers using /data/env for
data_location
to another endpoint - migrate consumers using /data/env for
window_title
to use /data/config - delete the /data/env endpoint
Secondly, perhaps it makes more sense to just pass path_prefix in the /data/environment in this PR, (app_routing_effects would block on getEnvironment instead of getConfig). That way, the plan would be simpler:
- migrate consumers using /data/env for
data_location
to another endpoint
wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the broad strokes, we are about aligned.
While /data/environment
is not the most meaningful name, it is actually being used for experiment metadata in a worse way (not just data_location).
tensorboard/tensorboard/plugins/core/core_plugin.py
Lines 140 to 147 in 83ce2a2
if experiment_metadata is not None: | |
environment.update( | |
{ | |
"experiment_name": experiment_metadata.experiment_name, | |
"experiment_description": experiment_metadata.experiment_description, | |
"creation_time": experiment_metadata.creation_time, | |
} | |
) |
My ideal would be to rename /data/experiment/[:id]/metadata
but "environment", in some sense, about the same thing and I would not advocate for the change unless others feel strongly about it.
I would really not want to roll all experiment metadata under experiment-specific endpoint that is meant to serve lists or runs (besides those endpoints don't exist in a meaningful way? /experiments return an empty array all the time)
One follow up work that is required is to remove window_title
from /environment
and its usage in the page title:
tensorboard/tensorboard/webapp/core/views/page_title_container.ts
Lines 39 to 43 in 83ce2a2
readonly title$ = this.store.pipe( | |
select(getEnvironment), | |
map((env) => env.window_title || 'TensorBoard'), | |
// (it's an empty string when the `--window_title` flag is not set) | |
distinctUntilChanged() |
filter((pathPrefix) => pathPrefix !== null), | ||
take(1), | ||
tap((pathPrefix) => { | ||
this.pathPrefix = pathPrefix; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would things be more maintainable if we didn't store pathPrefix on AppRoutingEffects, but made the Location's getPath, onPopState, replaceState methods aware of it? e.g.
this.location.setPathPrefix(pathPrefix);
I think Location's method surface is much smaller to check than making sure every usage of this.location.getPath()
is covered in this file. In case some other file in app_routing needed to inject Location, it wouldn't need to do all this wrapping as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that but was a bit weary of putting business logic into what seems like a clear API, location!
According to our usages of the state, we need to be able to
- read synchronously
- discern was modified
Those one or two are separate APIs that really do not belong in the Location, imo.
I'm not crazy about baking the What William suggests above nicely avoids this dependency on the prefix itself:
We can use this aspect of the suggestion even if we don't do server-side HTML modification, since we can just change If we do that, we actually do preserve the property that |
No, that is not what actually happens. If you navigate to
sounds like what I had recommended in the "dehydrate" section in original comment (#4421 (comment)).
Sorry, I don't understand the approach at all. Before I address any comments, I want to hear back from passbyers. |
Yes, this is what I actually observed at d0a9f847c, running:
and navigating to (I have |
Let me know if you need more info to repro. |
Suppose that we’re at @nfelt and I are saying that case (b) is nicer than case (a) because
Agreed, but |
@wchargin, Thanks, I can repro. It is very surprising to me that we serve
That benefit seems very hypothetical and marginal. Response changing given current route (
Is |
Good point. I stand corrected. |
As a tensorboard user, I would very much prefer the solution suggested by @wchargin (case b). Allowing the frontend to work under any relative path independent from the configured path-prefix - e.g. by using Other complex tools, such as code-server, are also able to have their frontend run under a relative path. As far as I know, they are also using the To add an example which, as far as I understood, would not work with the implemented solution:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as "request changes", to await feedback on a new approach using server side provided path.
9b600a5
to
ccf0685
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
685e807
to
43341bc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this looks good, thanks. (I think we'll eventually want to avoid CorePlugin directly using path_prefix but I'm happy to clean that up later.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note: I think we may need router_link_directive_container.ts
to become aware of AppRoots as well. Currently, when an <a>
has [routerLink]=['foo']
, we guarantee that the "href" attribute is a full, correct URL so things like ctrl+click properly open "http://tb/foo" in a new tab.
With path_prefix, I would imagine those hrefs now point to incorrect appRoot-less URLs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python changes and tests LGTM, thanks. Will defer to Erik's remaining comments before approving though.
# Technically, it is possible to flush parts using yields but http_utils | ||
# try to measure gzip and measure content length if a truthy value is | ||
# passed. | ||
res = http_util.Respond(request, None, "text/html") | ||
res.stream.write( | ||
'<meta name="tb-relative-root" content="%s/" />' % (relpath) | ||
) | ||
res.stream.write(index_asset_bytes) | ||
return res |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t quite understand why this change implies the accompanying change
to http_util.Respond
. The alternative that comes to mind is:
meta_header = '<meta name="tb-relative-root" content="%s/" />' % (relpath)
content = meta_header.encode("utf-8") + index_asset_bytes
return http_util.Respond(request, content, "text/html")
Judging from the comment, it sounds like you want to avoid that because
it automatically gzips, since the content type is text/html
. That
makes sense. But then can’t we just pass content_encoding="identity"
rather than adding new mechanisms? I think that that already does the
right thing:
>>> from tensorboard.backend import http_util
>>> from werkzeug import test as wtest
>>> from werkzeug import wrappers
>>> def req():
... headers = [("Accept-Encoding", "gzip")]
... environ = wtest.EnvironBuilder(headers=headers).get_environ()
... return wrappers.Request(environ)
...
>>> res = http_util.Respond(req(), b"<html>\n", "text/html")
>>> res.headers.get("Content-Encoding"), res.get_data()
('gzip', b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb3\xc9(\xc9\xcd\xb1\xe3\x02\x00\x994\xcb3\x07\x00\x00\x00')
>>> res = http_util.Respond(req(), b"<html>\n", "text/html", content_encoding="identity")
>>> res.headers.get("Content-Encoding"), res.get_data()
('identity', b'<html>\n')
Failing that, I might prefer to add a new no_gzip
flag or something
rather than introducing (a) the edge case in http_util
and (b) the
stream-writing code in this method.
Tested the sync (cl/347955446) and tested against the internal version of TB. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The frontend code lgtm, thanks for checking internally.
I also did a quick test sync and saw 1 projector test fail, but I'm not 100% sure its related: cl/348085083
Approving % wchargin@'s approval on the content_encoding=identity topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python revisions look good to me. Ideally I would like html.escape
around the HTML interpolation; even though there’s probably an argument
that it’s safe in this case due to provenance of relpath
, it’s nice
for it to be “clearly correct” rather than “merely correct”.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took a quick look and the updates and generally LGTM. +1 to William's point that it would be a bit better to escape the inserted relative path, just in case.
pathname: this.pathname, | ||
}); | ||
return ( | ||
this.appRoot.slice(0, -1) + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it maybe make sense to expose getAbsPathnameWithAppRoot()
somewhere to kind of share this logic? As it is, it's not terribly obvious here why we slice the last char off appRoot unless you can see the bigger picture. Maybe at least a comment would help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Moved it to AppRootProvider instead.
Problem: TensorBoard has a flag, path_prefix, which serves frontend and the data endpoints at your specified, if the flag is passed, pathname. While exact use case slips my mind, it is certainly used by dependents. Now, the problem is that new Angular based TensorBoard has a custom router for in-app navigation (we may add new routes for experiment list and others) it is unaware of the path prefix. Router works by matching set of predefined routes to current pathname and finds a match. However, the configuration is unaware (rightfully) of the path prefix and it results in zero match, which in turn cause navigation to the default path, "/". This totally breaks the assumption of the path prefix as all the request from the frontend will be sent to "/data/..." as opposed to "[path_prefix]/data/..." Remediation: Previously, there was no reason to propagate the prefix information to the frontend. Application only did hash changes and never required a navigation. With this change, we now: - server side render meta tag that depicts the relative path to the root of the application - bootstrap Angular app from the relative root path and uses that information for matching routes and navigation Tests: - manually tested with/without path_prefix - tested dynamic plugin with/without path_prefix (projector)
@psybuzz made significant changes to the FE code to adopt the feedback. Please TAL one last time. |
@@ -54,6 +56,10 @@ function isNavigation( | |||
|
|||
@Injectable() | |||
export class Location implements LocationInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One suggestion: perhaps we should add a comment to appRootProvider.getFullPathFromRouteOrNav()
, noting that this.appRootProvider.getAbsPathnameWithAppRoot()
is required to get the "full full" path.
Problem: TensorBoard has a flag, path_prefix, which serves frontend and the data endpoints at your specified, if the flag is passed, pathname. While exact use case slips my mind, it is certainly used by dependents. Now, the problem is that new Angular based TensorBoard has a custom router for in-app navigation (we may add new routes for experiment list and others) it is unaware of the path prefix. Router works by matching set of predefined routes to current pathname and finds a match. However, the configuration is unaware (rightfully) of the path prefix and it results in zero match, which in turn cause navigation to the default path, "/". This totally breaks the assumption of the path prefix as all the request from the frontend will be sent to "/data/..." as opposed to "[path_prefix]/data/..." Remediation: Previously, there was no reason to propagate the prefix information to the frontend. Application only did hash changes and never required a navigation. With this change, we now: - server side render `<meta>` tag that depicts the relative path to the root of the application. - bootstrap Angular app from the relative root path and uses that information for matching routes and navigation
Backport of #4423 to 2.4. Cf. #4547. --- Problem: TensorBoard has a flag, path_prefix, which serves frontend and the data endpoints at your specified, if the flag is passed, pathname. While exact use case slips my mind, it is certainly used by dependents. Now, the problem is that new Angular based TensorBoard has a custom router for in-app navigation (we may add new routes for experiment list and others) it is unaware of the path prefix. Router works by matching set of predefined routes to current pathname and finds a match. However, the configuration is unaware (rightfully) of the path prefix and it results in zero match, which in turn cause navigation to the default path, "/". This totally breaks the assumption of the path prefix as all the request from the frontend will be sent to "/data/..." as opposed to "[path_prefix]/data/..." Remediation: Previously, there was no reason to propagate the prefix information to the frontend. Application only did hash changes and never required a navigation. With this change, we now: - server side render meta tag that depicts the relative path to the root of the application - bootstrap Angular app from the relative root path and uses that information for matching routes and navigation Tests: - manually tested with/without path_prefix - tested dynamic plugin with/without path_prefix (projector) Co-authored-by: Stephan Lee <[email protected]>
Problem:
TensorBoard has a flag, path_prefix, which serves frontend and the data
endpoints at your specified, if the flag is passed, pathname. While
exact use case slips my mind, it is certainly used by dependents. Now,
the problem is that new Angular based TensorBoard has a custom router
for in-app navigation (we may add new routes for experiment list and
others) it is unaware of the path prefix.
Router works by matching set of predefined routes to current pathname
and finds a match. However, the configuration is unaware (rightfully) of
the path prefix and it results in zero match, which in turn cause
navigation to the default path, "/". This totally breaks the assumption
of the path prefix as all the request from the frontend will be sent to
"/data/..." as opposed to "[path_prefix]/data/..."
Remediation:
Previously, there was no reason to propagate the prefix information to
the frontend. Application only did hash changes and never required a
navigation. With this change, we now:
application
for matching routes and navigation
Tests: