Skip to content

Commit fee9055

Browse files
committed
JS: Refactor link & badge generation, use URLs (not string) for base URLs
This is two changes that were easier to make together - BASE_URL and BADGE_BASE_URL are now URL objects rather than strings that are manipulated. With this done, we no longer use string manipulation for URLs anywhere! - Both BASE_URL and BADGE_BASE_URL are now always set, as we had a bunch of code that was using BADGE_BASE_URL if available but falls back to BASE_URL + origin if it was not set. This fallback is now implemented globally, and correctly. - BASE_URL is also now always fully qualified, and we document that the python code ensures it has a trailing slash always. - The function to make links and generate badge markup is moved into `@jupyterhub/binderhub-client` as it is reasonably generic and not super specific to our frontend alone. This also involves them not reading BASE_URL and BADGE_BASE_URL globally, but having that information be passed in. Tests are also added here to catch any future issues that may arise. - Note for future fix - BADGE_BASE_URL is really PUBLIC_BASE_URL or similar, as it is used both for the location of the badge image (original intent) but also for the links we generate to share. This is relevant only for federation, where we want shared links to point to mybinder.org even though the API call itself may go to a specific member of the federation. I will do this deprecation + rename in a future PR so as to not make this PR bigger. Ref #774
1 parent 451eba4 commit fee9055

File tree

8 files changed

+204
-106
lines changed

8 files changed

+204
-106
lines changed

binderhub/static/js/index.js

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import "bootstrap/dist/css/bootstrap-theme.min.css";
1818
import "../index.css";
1919
import { setUpLog } from "./src/log";
2020
import { updateUrls } from "./src/urls";
21-
import { BASE_URL } from "./src/constants";
21+
import { BASE_URL, BADGE_BASE_URL } from "./src/constants";
2222
import { getBuildFormValues } from "./src/form";
2323
import { updateRepoText } from "./src/repo";
2424

2525
async function build(providerSpec, log, fitAddon, path, pathType) {
26-
updateFavicon(BASE_URL + "favicon_building.ico");
26+
updateFavicon(new URL("favicon_building.ico", BASE_URL));
2727
// split provider prefix off of providerSpec
2828
const spec = providerSpec.slice(providerSpec.indexOf("/") + 1);
2929
// Update the text of the loading page if it exists
@@ -39,13 +39,7 @@ async function build(providerSpec, log, fitAddon, path, pathType) {
3939
$(".on-build").removeClass("hidden");
4040

4141
const buildToken = $("#build-token").data("token");
42-
// If BASE_URL is absolute, use that as the base for build endpoint URL.
43-
// Else, first resolve BASE_URL relative to current URL, then use *that* as the
44-
// base for the build endpoint url.
45-
const buildEndpointUrl = new URL(
46-
"build",
47-
new URL(BASE_URL, window.location.href),
48-
);
42+
const buildEndpointUrl = new URL("build", BASE_URL);
4943
const image = new BinderRepository(
5044
providerSpec,
5145
buildEndpointUrl,
@@ -82,7 +76,7 @@ async function build(providerSpec, log, fitAddon, path, pathType) {
8276
$("#loader").addClass("paused");
8377

8478
// If we fail for any reason, show an error message and logs
85-
updateFavicon(BASE_URL + "favicon_fail.ico");
79+
updateFavicon(new URL("favicon_fail.ico", BASE_URL));
8680
log.show();
8781
if ($("div#loader-text").length > 0) {
8882
$("#loader").addClass("error");
@@ -96,7 +90,7 @@ async function build(providerSpec, log, fitAddon, path, pathType) {
9690
case "built": {
9791
$("#phase-already-built").removeClass("hidden");
9892
$("#phase-launching").removeClass("hidden");
99-
updateFavicon(BASE_URL + "favicon_success.ico");
93+
updateFavicon(new URL("favicon_success.ico", BASE_URL));
10094
break;
10195
}
10296
case "ready": {
@@ -127,15 +121,15 @@ function indexMain() {
127121
const [log, fitAddon] = setUpLog();
128122

129123
// setup badge dropdown and default values.
130-
updateUrls();
124+
updateUrls(BADGE_BASE_URL);
131125

132126
$("#provider_prefix_sel li").click(function (event) {
133127
event.preventDefault();
134128

135129
$("#provider_prefix-selected").text($(this).text());
136130
$("#provider_prefix").val($(this).attr("value"));
137131
updateRepoText();
138-
updateUrls();
132+
updateUrls(BADGE_BASE_URL);
139133
});
140134

141135
$("#url-or-file-btn")
@@ -145,21 +139,21 @@ function indexMain() {
145139

146140
$("#url-or-file-selected").text($(this).text());
147141
updatePathText();
148-
updateUrls();
142+
updateUrls(BADGE_BASE_URL);
149143
});
150144
updatePathText();
151145
updateRepoText();
152146

153147
$("#repository").on("keyup paste change", function () {
154-
updateUrls();
148+
updateUrls(BADGE_BASE_URL);
155149
});
156150

157151
$("#ref").on("keyup paste change", function () {
158-
updateUrls();
152+
updateUrls(BADGE_BASE_URL);
159153
});
160154

161155
$("#filepath").on("keyup paste change", function () {
162-
updateUrls();
156+
updateUrls(BADGE_BASE_URL);
163157
});
164158

165159
$("#toggle-badge-snippet").on("click", function () {
@@ -180,7 +174,7 @@ function indexMain() {
180174
$("#build-form").submit(async function (e) {
181175
e.preventDefault();
182176
const formValues = getBuildFormValues();
183-
updateUrls(formValues);
177+
updateUrls(BADGE_BASE_URL, formValues);
184178
await build(
185179
formValues.providerPrefix + "/" + formValues.repo + "/" + formValues.ref,
186180
log,

binderhub/static/js/src/badge.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

binderhub/static/js/src/constants.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
/**
2-
* @type {string}
3-
* Base URL of this binderhub installation
2+
* @type {URL}
3+
* Base URL of this binderhub installation.
4+
*
5+
* Guaranteed to have a trailing slash by the binderhub python configuration.
46
*/
5-
export const BASE_URL = $("#base-url").data().url;
7+
export const BASE_URL = new URL(
8+
document.getElementById("base-url").dataset.url,
9+
document.location.origin,
10+
);
611

12+
const badge_base_url = document.getElementById("badge-base-url").dataset.url;
713
/**
8-
* @type {string}
9-
* Optional base URL to use for both badge images as well as launch links.
14+
* @type {URL}
15+
* Base URL to use for both badge images as well as launch links.
1016
*
11-
* Is different from BASE_URL primarily when used as part of a federation.
17+
* If not explicitly set, will default to BASE_URL. Primarily set up different than BASE_URL
18+
* when used as part of a federation
1219
*/
13-
export const BADGE_BASE_URL = $("#badge-base-url").data().url;
20+
export const BADGE_BASE_URL = badge_base_url
21+
? new URL(badge_base_url, document.location.origin)
22+
: BASE_URL;

binderhub/static/js/src/favicon.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Dynamically set current page's favicon.
33
*
4-
* @param {String} href Path to Favicon to use
4+
* @param {URL} href Path to Favicon to use
55
*/
66
function updateFavicon(href) {
77
let link = document.querySelector("link[rel*='icon']");

binderhub/static/js/src/repo.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function setLabels() {
2626
*/
2727
export function updateRepoText() {
2828
if (Object.keys(configDict).length === 0) {
29-
const configUrl = BASE_URL + "_config";
29+
const configUrl = new URL("_config", BASE_URL);
3030
fetch(configUrl).then((resp) => {
3131
resp.json().then((data) => {
3232
configDict = data;

binderhub/static/js/src/urls.js

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,33 @@
1-
import { makeBadgeMarkup } from "./badge";
21
import { getBuildFormValues } from "./form";
3-
import { BADGE_BASE_URL, BASE_URL } from "./constants";
4-
5-
/**
6-
* Generate a shareable binder URL for given repository
7-
8-
* @param {string} providerPrefix prefix denoting what provider was selected
9-
* @param {string} repo repo to build
10-
* @param {[string]} ref optional ref in this repo to build
11-
* @param {string} path Path to launch after this repo has been built
12-
* @param {string} pathType Type of thing to open path with (raw url, notebook file, lab, etc)
13-
*
14-
* @returns {string|null} A URL that can be shared with others, and clicking which will launch the repo
15-
*/
16-
function v2url(providerPrefix, repository, ref, path, pathType) {
17-
// return a v2 url from a providerPrefix, repository, ref, and (file|url)path
18-
if (repository.length === 0) {
19-
// no repo, no url
20-
return null;
21-
}
22-
let url;
23-
if (BADGE_BASE_URL) {
24-
url =
25-
BADGE_BASE_URL + "v2/" + providerPrefix + "/" + repository + "/" + ref;
26-
} else {
27-
url =
28-
window.location.origin +
29-
BASE_URL +
30-
"v2/" +
31-
providerPrefix +
32-
"/" +
33-
repository +
34-
"/" +
35-
ref;
36-
}
37-
if (path && path.length > 0) {
38-
// encode the path, it will be decoded in loadingMain
39-
url = url + "?" + pathType + "path=" + encodeURIComponent(path);
40-
}
41-
return url;
42-
}
2+
import {
3+
makeShareableBinderURL,
4+
makeBadgeMarkup,
5+
} from "@jupyterhub/binderhub-client";
436

447
/**
458
* Update the shareable URL and badge snippets in the UI based on values user has entered in the form
469
*/
47-
export function updateUrls(formValues) {
10+
export function updateUrls(publicBaseUrl, formValues) {
4811
if (typeof formValues === "undefined") {
4912
formValues = getBuildFormValues();
5013
}
51-
const url = v2url(
52-
formValues.providerPrefix,
53-
formValues.repo,
54-
formValues.ref,
55-
formValues.path,
56-
formValues.pathType,
57-
);
14+
if (formValues.repo) {
15+
const url = makeShareableBinderURL(
16+
publicBaseUrl,
17+
formValues.providerPrefix,
18+
formValues.repo,
19+
formValues.ref,
20+
formValues.path,
21+
formValues.pathType,
22+
);
5823

59-
if ((url || "").trim().length > 0) {
6024
// update URLs and links (badges, etc.)
6125
$("#badge-link").attr("href", url);
6226
$("#basic-url-snippet").text(url);
6327
$("#markdown-badge-snippet").text(
64-
makeBadgeMarkup(BADGE_BASE_URL, BASE_URL, url, "markdown"),
65-
);
66-
$("#rst-badge-snippet").text(
67-
makeBadgeMarkup(BADGE_BASE_URL, BASE_URL, url, "rst"),
28+
makeBadgeMarkup(publicBaseUrl, url, "markdown"),
6829
);
30+
$("#rst-badge-snippet").text(makeBadgeMarkup(publicBaseUrl, url, "rst"));
6931
} else {
7032
["#basic-url-snippet", "#markdown-badge-snippet", "#rst-badge-snippet"].map(
7133
function (item) {

js/packages/binderhub-client/lib/index.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,65 @@ export class BinderRepository {
149149
return url;
150150
}
151151
}
152+
153+
/**
154+
* Generate a shareable binder URL for given repository
155+
*
156+
* @param {URL} publicBaseUrl Base URL to use for making public URLs. Must end with a trailing slash.
157+
* @param {string} providerPrefix prefix denoting what provider was selected
158+
* @param {string} repo repo to build
159+
* @param {string} ref optional ref in this repo to build
160+
* @param {[string]} path Path to launch after this repo has been built
161+
* @param {[string]} pathType Type of thing to open path with (raw url, notebook file, lab, etc)
162+
*
163+
* @returns {URL} A URL that can be shared with others, and clicking which will launch the repo
164+
*/
165+
export function makeShareableBinderURL(
166+
publicBaseUrl,
167+
providerPrefix,
168+
repository,
169+
ref,
170+
path,
171+
pathType,
172+
) {
173+
if (!publicBaseUrl.pathname.endsWith("/")) {
174+
throw new Error(
175+
`publicBaseUrl must end with a trailing slash, got ${publicBaseUrl}`,
176+
);
177+
}
178+
const url = new URL(
179+
`v2/${providerPrefix}/${repository}/${ref}`,
180+
publicBaseUrl,
181+
);
182+
if (path && path.length > 0) {
183+
url.searchParams.append(`${pathType}path`, path);
184+
}
185+
return url;
186+
}
187+
188+
/**
189+
* Generate markup that people can put on their README or documentation to link to a specific binder
190+
*
191+
* @param {URL} publicBaseUrl Base URL to use for making public URLs
192+
* @param {URL} url Link target URL that represents this binder installation
193+
* @param {string} syntax Kind of markup to generate. Supports 'markdown' and 'rst'
194+
* @returns {string}
195+
*/
196+
export function makeBadgeMarkup(publicBaseUrl, url, syntax) {
197+
if (!publicBaseUrl.pathname.endsWith("/")) {
198+
throw new Error(
199+
`publicBaseUrl must end with a trailing slash, got ${publicBaseUrl}`,
200+
);
201+
}
202+
const badgeImageUrl = new URL("badge_logo.svg", publicBaseUrl);
203+
204+
if (syntax === "markdown") {
205+
return `[![Binder](${badgeImageUrl})](${url})`;
206+
} else if (syntax === "rst") {
207+
return `.. image:: ${badgeImageUrl}\n :target: ${url}`;
208+
} else {
209+
throw new Error(
210+
`Only markdown or rst badges are supported, got ${syntax} instead`,
211+
);
212+
}
213+
}

0 commit comments

Comments
 (0)