Skip to content
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

JS: Refactor link & badge generation, use URLs (not string) for base URLs #1778

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions binderhub/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import "bootstrap/dist/css/bootstrap-theme.min.css";
import "../index.css";
import { setUpLog } from "./src/log";
import { updateUrls } from "./src/urls";
import { BASE_URL } from "./src/constants";
import { BASE_URL, BADGE_BASE_URL } from "./src/constants";
import { getBuildFormValues } from "./src/form";
import { updateRepoText } from "./src/repo";

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

const buildToken = $("#build-token").data("token");
// If BASE_URL is absolute, use that as the base for build endpoint URL.
// Else, first resolve BASE_URL relative to current URL, then use *that* as the
// base for the build endpoint url.
const buildEndpointUrl = new URL(
"build",
new URL(BASE_URL, window.location.href),
);
const buildEndpointUrl = new URL("build", BASE_URL);
const image = new BinderRepository(
providerSpec,
buildEndpointUrl,
Expand Down Expand Up @@ -82,7 +76,7 @@ async function build(providerSpec, log, fitAddon, path, pathType) {
$("#loader").addClass("paused");

// If we fail for any reason, show an error message and logs
updateFavicon(BASE_URL + "favicon_fail.ico");
updateFavicon(new URL("favicon_fail.ico", BASE_URL));
log.show();
if ($("div#loader-text").length > 0) {
$("#loader").addClass("error");
Expand All @@ -96,7 +90,7 @@ async function build(providerSpec, log, fitAddon, path, pathType) {
case "built": {
$("#phase-already-built").removeClass("hidden");
$("#phase-launching").removeClass("hidden");
updateFavicon(BASE_URL + "favicon_success.ico");
updateFavicon(new URL("favicon_success.ico", BASE_URL));
break;
}
case "ready": {
Expand Down Expand Up @@ -127,15 +121,15 @@ function indexMain() {
const [log, fitAddon] = setUpLog();

// setup badge dropdown and default values.
updateUrls();
updateUrls(BADGE_BASE_URL);

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

$("#provider_prefix-selected").text($(this).text());
$("#provider_prefix").val($(this).attr("value"));
updateRepoText();
updateUrls();
updateUrls(BADGE_BASE_URL);
});

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

$("#url-or-file-selected").text($(this).text());
updatePathText();
updateUrls();
updateUrls(BADGE_BASE_URL);
});
updatePathText();
updateRepoText();

$("#repository").on("keyup paste change", function () {
updateUrls();
updateUrls(BADGE_BASE_URL);
});

$("#ref").on("keyup paste change", function () {
updateUrls();
updateUrls(BADGE_BASE_URL);
});

$("#filepath").on("keyup paste change", function () {
updateUrls();
updateUrls(BADGE_BASE_URL);
});

$("#toggle-badge-snippet").on("click", function () {
Expand All @@ -180,7 +174,7 @@ function indexMain() {
$("#build-form").submit(async function (e) {
e.preventDefault();
const formValues = getBuildFormValues();
updateUrls(formValues);
updateUrls(BADGE_BASE_URL, formValues);
await build(
formValues.providerPrefix + "/" + formValues.repo + "/" + formValues.ref,
log,
Expand Down
24 changes: 0 additions & 24 deletions binderhub/static/js/src/badge.js

This file was deleted.

23 changes: 16 additions & 7 deletions binderhub/static/js/src/constants.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
/**
* @type {string}
* Base URL of this binderhub installation
* @type {URL}
* Base URL of this binderhub installation.
*
* Guaranteed to have a leading & trailing slash by the binderhub python configuration.
*/
export const BASE_URL = $("#base-url").data().url;
export const BASE_URL = new URL(
document.getElementById("base-url").dataset.url,
document.location.origin,
);
consideRatio marked this conversation as resolved.
Show resolved Hide resolved

const badge_base_url = document.getElementById("badge-base-url").dataset.url;
/**
* @type {string}
* Optional base URL to use for both badge images as well as launch links.
* @type {URL}
* Base URL to use for both badge images as well as launch links.
*
* Is different from BASE_URL primarily when used as part of a federation.
* If not explicitly set, will default to BASE_URL. Primarily set up different than BASE_URL
* when used as part of a federation
*/
export const BADGE_BASE_URL = $("#badge-base-url").data().url;
export const BADGE_BASE_URL = badge_base_url
? new URL(badge_base_url, document.location.origin)
: BASE_URL;
2 changes: 1 addition & 1 deletion binderhub/static/js/src/favicon.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Dynamically set current page's favicon.
*
* @param {String} href Path to Favicon to use
* @param {URL} href Path to Favicon to use
*/
function updateFavicon(href) {
let link = document.querySelector("link[rel*='icon']");
Expand Down
2 changes: 1 addition & 1 deletion binderhub/static/js/src/repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function setLabels() {
*/
export function updateRepoText() {
if (Object.keys(configDict).length === 0) {
const configUrl = BASE_URL + "_config";
const configUrl = new URL("_config", BASE_URL);
fetch(configUrl).then((resp) => {
resp.json().then((data) => {
configDict = data;
Expand Down
70 changes: 16 additions & 54 deletions binderhub/static/js/src/urls.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,33 @@
import { makeBadgeMarkup } from "./badge";
import { getBuildFormValues } from "./form";
import { BADGE_BASE_URL, BASE_URL } from "./constants";

/**
* Generate a shareable binder URL for given repository

* @param {string} providerPrefix prefix denoting what provider was selected
* @param {string} repo repo to build
* @param {[string]} ref optional ref in this repo to build
* @param {string} path Path to launch after this repo has been built
* @param {string} pathType Type of thing to open path with (raw url, notebook file, lab, etc)
*
* @returns {string|null} A URL that can be shared with others, and clicking which will launch the repo
*/
function v2url(providerPrefix, repository, ref, path, pathType) {
// return a v2 url from a providerPrefix, repository, ref, and (file|url)path
if (repository.length === 0) {
// no repo, no url
return null;
}
let url;
if (BADGE_BASE_URL) {
url =
BADGE_BASE_URL + "v2/" + providerPrefix + "/" + repository + "/" + ref;
} else {
url =
window.location.origin +
BASE_URL +
"v2/" +
providerPrefix +
"/" +
repository +
"/" +
ref;
}
if (path && path.length > 0) {
// encode the path, it will be decoded in loadingMain
url = url + "?" + pathType + "path=" + encodeURIComponent(path);
}
return url;
}
import {
makeShareableBinderURL,
makeBadgeMarkup,
} from "@jupyterhub/binderhub-client";

/**
* Update the shareable URL and badge snippets in the UI based on values user has entered in the form
*/
export function updateUrls(formValues) {
export function updateUrls(publicBaseUrl, formValues) {
if (typeof formValues === "undefined") {
formValues = getBuildFormValues();
}
const url = v2url(
formValues.providerPrefix,
formValues.repo,
formValues.ref,
formValues.path,
formValues.pathType,
);
if (formValues.repo) {
const url = makeShareableBinderURL(
publicBaseUrl,
formValues.providerPrefix,
formValues.repo,
formValues.ref,
formValues.path,
formValues.pathType,
);

if ((url || "").trim().length > 0) {
// update URLs and links (badges, etc.)
$("#badge-link").attr("href", url);
$("#basic-url-snippet").text(url);
$("#markdown-badge-snippet").text(
makeBadgeMarkup(BADGE_BASE_URL, BASE_URL, url, "markdown"),
);
$("#rst-badge-snippet").text(
makeBadgeMarkup(BADGE_BASE_URL, BASE_URL, url, "rst"),
makeBadgeMarkup(publicBaseUrl, url, "markdown"),
);
$("#rst-badge-snippet").text(makeBadgeMarkup(publicBaseUrl, url, "rst"));
} else {
["#basic-url-snippet", "#markdown-badge-snippet", "#rst-badge-snippet"].map(
function (item) {
Expand Down
62 changes: 62 additions & 0 deletions js/packages/binderhub-client/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,65 @@ export class BinderRepository {
return url;
}
}

/**
* Generate a shareable binder URL for given repository
*
* @param {URL} publicBaseUrl Base URL to use for making public URLs. Must end with a trailing slash.
* @param {string} providerPrefix prefix denoting what provider was selected
* @param {string} repo repo to build
* @param {string} ref optional ref in this repo to build
* @param {[string]} path Path to launch after this repo has been built
* @param {[string]} pathType Type of thing to open path with (raw url, notebook file, lab, etc)
*
* @returns {URL} A URL that can be shared with others, and clicking which will launch the repo
*/
export function makeShareableBinderURL(
publicBaseUrl,
providerPrefix,
repository,
ref,
path,
pathType,
) {
if (!publicBaseUrl.pathname.endsWith("/")) {
throw new Error(
`publicBaseUrl must end with a trailing slash, got ${publicBaseUrl}`,
);
}
const url = new URL(
`v2/${providerPrefix}/${repository}/${ref}`,
publicBaseUrl,
);
if (path && path.length > 0) {
url.searchParams.append(`${pathType}path`, path);
}
return url;
}

/**
* Generate markup that people can put on their README or documentation to link to a specific binder
*
* @param {URL} publicBaseUrl Base URL to use for making public URLs
* @param {URL} url Link target URL that represents this binder installation
* @param {string} syntax Kind of markup to generate. Supports 'markdown' and 'rst'
* @returns {string}
*/
export function makeBadgeMarkup(publicBaseUrl, url, syntax) {
if (!publicBaseUrl.pathname.endsWith("/")) {
throw new Error(
`publicBaseUrl must end with a trailing slash, got ${publicBaseUrl}`,
);
}
const badgeImageUrl = new URL("badge_logo.svg", publicBaseUrl);

if (syntax === "markdown") {
return `[![Binder](${badgeImageUrl})](${url})`;
} else if (syntax === "rst") {
return `.. image:: ${badgeImageUrl}\n :target: ${url}`;
} else {
throw new Error(
`Only markdown or rst badges are supported, got ${syntax} instead`,
);
}
}
Loading