diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 8e4483b9d..3df6b59a0 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -11,12 +11,14 @@ on: - ".eslintrc.js" - "package.json" - "binderhub/static/js/**" + - "js/**" push: paths: - ".github/workflows/eslint.yml" - ".eslintrc.js" - "package.json" - "binderhub/static/js/**" + - "js/**" branches-ignore: - "dependabot/**" - "pre-commit-ci-update-config" diff --git a/.gitignore b/.gitignore index 79b856aad..19b482212 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ diff --git a/binderhub/static/js/index.js b/binderhub/static/js/index.js index 6d2673500..216e31d02 100644 --- a/binderhub/static/js/index.js +++ b/binderhub/static/js/index.js @@ -15,7 +15,7 @@ import { FitAddon } from 'xterm-addon-fit'; import ClipboardJS from 'clipboard'; import 'event-source-polyfill'; -import BinderImage from './src/image'; +import BinderImage from '@jupyterhub/binderhub-client'; import { makeBadgeMarkup } from './src/badge'; import { getPathType, updatePathText } from './src/path'; import { nextHelpText } from './src/loading'; diff --git a/binderhub/static/js/src/image.js b/js/packages/binderhub-client/lib/index.js similarity index 65% rename from binderhub/static/js/src/image.js rename to js/packages/binderhub-client/lib/index.js index bece748ea..7d3bd5136 100644 --- a/binderhub/static/js/src/image.js +++ b/js/packages/binderhub-client/lib/index.js @@ -1,8 +1,18 @@ import { NativeEventSource, EventSourcePolyfill } from 'event-source-polyfill'; +// Use native browser EventSource if available, and use the polyfill if not available const EventSource = NativeEventSource || EventSourcePolyfill; +/** + * Build and launch a repository by talking to a BinderHub API endpoint + */ export default class BinderImage { + /** + * + * @param {string} providerSpec Spec of the form // to pass to the binderhub API. + * @param {string} baseUrl Base URL (including the trailing slash) of the binderhub installation to talk to. + * @param {string} buildToken Optional JWT based build token if this binderhub installation requires using build tokesn + */ constructor(providerSpec, baseUrl, buildToken) { this.providerSpec = providerSpec; this.baseUrl = baseUrl; @@ -11,6 +21,9 @@ export default class BinderImage { this.state = null; } + /** + * Call the BinderHub API + */ fetch() { let apiUrl = this.baseUrl + "build/" + this.providerSpec; if (this.buildToken) { @@ -20,7 +33,7 @@ export default class BinderImage { this.eventSource = new EventSource(apiUrl); this.eventSource.onerror = (err) => { console.error("Failed to construct event stream", err); - this.changeState("failed", { + this._changeState("failed", { message: "Failed to connect to event stream\n" }); }; @@ -32,16 +45,27 @@ export default class BinderImage { if (data.phase) { state = data.phase.toLowerCase(); } - this.changeState(state, data); + this._changeState(state, data); }); } + /** + * Close the EventSource connection to the BinderHub API if it is open + */ close() { if (this.eventSource !== undefined) { this.eventSource.close(); } } + /** + * Redirect user to a running jupyter server with given token + + * @param {URL} url URL to the running jupyter server + * @param {string} token Secret token used to authenticate to the jupyter server + * @param {string} path The path of the file or url suffix to launch the user into + * @param {string} pathType One of "lab", "file" or "url", denoting what kinda path we are launching the user into + */ launch(url, token, path, pathType) { // redirect a user to a running server with a token if (path) { @@ -69,6 +93,13 @@ export default class BinderImage { window.location.href = url; } + + /** + * Add callback whenever state of the current build changes + * + * @param {str} state The state to add this callback to. '*' to add callback for all state changes + * @param {*} cb Callback function to call whenever this state is reached + */ onStateChange(state, cb) { if (this.callbacks[state] === undefined) { this.callbacks[state] = [cb]; @@ -77,6 +108,11 @@ export default class BinderImage { } } + /** + * @param {string} oldState Old state the building process was in + * @param {string} newState New state the building process is in + * @returns True if transition from oldState to newState is valid, False otherwise + */ validateStateTransition(oldState, newState) { if (oldState === "start") { return ( @@ -93,7 +129,7 @@ export default class BinderImage { } } - changeState(state, data) { + _changeState(state, data) { [state, "*"].map(key => { const callbacks = this.callbacks[key]; if (callbacks) { diff --git a/js/packages/binderhub-client/package.json b/js/packages/binderhub-client/package.json new file mode 100644 index 000000000..dbb2c05dc --- /dev/null +++ b/js/packages/binderhub-client/package.json @@ -0,0 +1,19 @@ +{ + "name": "@jupyterhub/binderhub-client", + "version": "0.3.0", + "description": "Simple API client for the BinderHub EventSource API", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/jupyterhub/binderhub.git" + }, + "author": "Project Jupyter Contributors", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/jupyterhub/binderhub/issues" + }, + "homepage": "https://github.com/jupyterhub/binderhub.js#readme", + "dependencies": { + "event-source-polyfill": "^1.0.31" + } +} diff --git a/package.json b/package.json index 2413ec203..cfdaeacc6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "webpack": "^5.78.0", "webpack-cli": "^5.0.1" }, + "workspaces": [ + "js/packages/binderhub-client" + ], "scripts": { "webpack": "webpack", "webpack:watch": "webpack --watch",