diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c98e4c27..24794910d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 matched here. Site chrome (footer badge, back-to-top) keeps its own rules ([#167](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/167)). +### Removed +- The **Private JupyterHub** launch option. The toolbar's Launch control is now a + direct link to Google Colab instead of a popover with a radio group and a + hand-typed service URL. The demand signal this was gated on came back empty: + the book-theme only renders its hub button when `jupyterhub_url` is set in + `_config.yml`, and an org-wide search finds that key in no lecture repo at all + — every flagship series configures `colab_url` alone — so the button has never + appeared on a live QuantEcon site. The MyST version was also not the same + feature: it had no config key, rendered unconditionally, and asked the reader + to type a hub URL into a box whose value was `React.useState('')` and so was + discarded when the popover closed. Verified against a live build, it also + hardcoded a `/jupyter/hub/` path segment the Sphinx theme leaves to the + configured URL, so it only ever addressed hubs at `/jupyter/hub/` and + could not reach a stock JupyterHub, let alone a local Jupyter server. Removing + it drops `buildJupyterHubUrl`, the Radix popover and radio group, and the + `launch-open.png` visual baseline; `launch-colab` now asserts the anchor's + `href`, which also pins that the control is a link rather than a chooser. + Re-adding hub, Binder and local-server targets is tracked as future work + ([#87](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/87)). + ### Fixed - Every toolbar control was nesting a second ` - - ); -} + const config: LaunchConfig = { + repoUrl: launch_repo_url, + repoSuffix: launch_repo_suffix, + branch: launch_branch, + notebooksPath: launch_notebooks_path, + sourcePath: launch_source_path, + }; -export function LaunchButton({ size, showLabel }: { size: number; showLabel?: boolean }) { + // The tooltip merges onto the anchor (`asChild`) rather than rendering its + // own trigger inside it -- see Tooltip.tsx and the GitHubButton for the + // same shape. The anchor is the one interactive element. return ( - - - - - {showLabel && Launch} - - - - - - - - - + + + + {showLabel && Launch} + + ); } diff --git a/app/components/toolbar/launchUrls.ts b/app/components/toolbar/launchUrls.ts index 5f25dc02a..667ec00d2 100644 --- a/app/components/toolbar/launchUrls.ts +++ b/app/components/toolbar/launchUrls.ts @@ -76,20 +76,3 @@ export function buildColabUrl( return `${COLAB_BASE_URL}${orgRepo}/blob/${branch}/${relPath}`; } -/** Private JupyterHub (nbgitpuller `git-pull`) launch URL for the given page. */ -export function buildJupyterHubUrl( - hubBaseUrl: string, - sourceOrgRepo: string, - location: string, - config: LaunchConfig = {}, -): string { - const orgRepo = notebookOrgRepo(sourceOrgRepo, config); - const repoName = orgRepo.split('/')[1] ?? ''; - const branch = config.branch ?? DEFAULT_BRANCH; - const relPath = notebookRelPath(location, config); - // Drop a trailing slash on the user-provided hub URL to avoid `//jupyter`. - // Query values are left unencoded to match the book-theme `launch.py` / - // nbgitpuller format that the production QuantEcon hubs already accept. - const base = hubBaseUrl.replace(/\/+$/, ''); - return `${base}/jupyter/hub/user-redirect/git-pull?repo=https://github.com/${orgRepo}&branch=${branch}&urlpath=tree/${repoName}/${relPath}`; -} diff --git a/app/types.ts b/app/types.ts index ae624cde8..aa05a78e0 100644 --- a/app/types.ts +++ b/app/types.ts @@ -15,7 +15,7 @@ export interface TemplateOptions { git_metadata?: GitMetadata; // Notebook launcher configuration (set under `site.options` in myst.yml). - // Generalises the previously hardcoded Colab/JupyterHub launch URLs so + // Generalises the previously hardcoded Colab launch URLs so // non-default branches, repo naming and nested lecture dirs work. All keys // are optional; the defaults reproduce the historical behaviour // (`.notebooks`, branch `main`, and no source/notebooks path diff --git a/package-lock.json b/package-lock.json index 7179c2bf2..72131b429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,6 @@ "@myst-theme/styles": "^1.3.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-visually-hidden": "^1.2.4", @@ -8330,38 +8328,6 @@ } } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", diff --git a/package.json b/package.json index 3729a5b44..451e51d8f 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,6 @@ "@myst-theme/styles": "^1.3.0", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-visually-hidden": "^1.2.4", diff --git a/tests/unit/launch-urls.test.mjs b/tests/unit/launch-urls.test.mjs index eac96878f..d7d7ece67 100644 --- a/tests/unit/launch-urls.test.mjs +++ b/tests/unit/launch-urls.test.mjs @@ -19,13 +19,11 @@ import { test } from 'node:test'; import { buildColabUrl, - buildJupyterHubUrl, notebookOrgRepo, notebookRelPath, } from '../../app/components/toolbar/launchUrls.ts'; const SOURCE = 'QuantEcon/lecture-foo'; -const HUB = 'https://hub.example.org'; test('defaults reproduce the historical Colab URL (backward-compat)', () => { assert.equal( @@ -34,20 +32,6 @@ test('defaults reproduce the historical Colab URL (backward-compat)', () => { ); }); -test('defaults reproduce the historical JupyterHub URL (backward-compat)', () => { - assert.equal( - buildJupyterHubUrl(HUB, SOURCE, '/notebook.ipynb'), - 'https://hub.example.org/jupyter/hub/user-redirect/git-pull?repo=https://github.com/QuantEcon/lecture-foo.notebooks&branch=main&urlpath=tree/lecture-foo.notebooks/notebook.ipynb', - ); -}); - -test('a trailing slash on the hub URL does not produce a double slash', () => { - assert.equal( - buildJupyterHubUrl(`${HUB}/`, SOURCE, '/notebook.ipynb'), - 'https://hub.example.org/jupyter/hub/user-redirect/git-pull?repo=https://github.com/QuantEcon/lecture-foo.notebooks&branch=main&urlpath=tree/lecture-foo.notebooks/notebook.ipynb', - ); -}); - test('a source markdown page maps to the .ipynb in the notebook repo', () => { assert.equal( buildColabUrl(SOURCE, '/intro.md'), @@ -129,8 +113,4 @@ test('combined config: source_path strip + notebooks_path + branch + nested dir' buildColabUrl(SOURCE, '/lectures/topic/page.md', config), 'https://colab.research.google.com/github/QuantEcon/lecture-foo.notebooks/blob/release/nb/topic/page.ipynb', ); - assert.equal( - buildJupyterHubUrl(HUB, SOURCE, '/lectures/topic/page.md', config), - 'https://hub.example.org/jupyter/hub/user-redirect/git-pull?repo=https://github.com/QuantEcon/lecture-foo.notebooks&branch=release&urlpath=tree/lecture-foo.notebooks/nb/topic/page.ipynb', - ); }); diff --git a/tests/visual/__snapshots__/desktop-chrome-darwin/launch-open.png b/tests/visual/__snapshots__/desktop-chrome-darwin/launch-open.png deleted file mode 100644 index f7c3d0c81..000000000 Binary files a/tests/visual/__snapshots__/desktop-chrome-darwin/launch-open.png and /dev/null differ diff --git a/tests/visual/__snapshots__/desktop-chrome-linux/launch-open.png b/tests/visual/__snapshots__/desktop-chrome-linux/launch-open.png deleted file mode 100644 index 50b289b92..000000000 Binary files a/tests/visual/__snapshots__/desktop-chrome-linux/launch-open.png and /dev/null differ diff --git a/tests/visual/theme.spec.ts b/tests/visual/theme.spec.ts index f02ea7324..b7d2a27f1 100644 --- a/tests/visual/theme.spec.ts +++ b/tests/visual/theme.spec.ts @@ -258,37 +258,25 @@ test.describe("QuantEcon theme — visual regression", () => { await expect(drawer).toBeHidden(); }); - // Launch popover: Colab is the default and primary target (BinderHub is - // deliberately not offered — #26). window.open is stubbed so the launch URL - // assertion is deterministic and offline; the URL's repo part comes from the + // Launch is a direct link to Colab — the only launch target (BinderHub was + // deliberately not offered, #26; the private JupyterHub option was removed in + // #87). Asserting the anchor's href rather than a stubbed window.open keeps + // this offline and deterministic, and pins that the control is a *link*, so a + // regression back to a chooser would fail here. The repo part comes from the // fixture's `github` field, so only the stable pieces (host, .notebooks - // convention, branch, path) are pinned. + // convention, branch, path) are matched. test("launch-colab", async ({ page }, testInfo) => { test.skip( testInfo.project.name !== "desktop-chrome", - "the launch popover lives in the desktop toolbar; mobile wraps it in MobileActionsMenu" + "the launch control lives in the desktop toolbar; mobile wraps it in MobileActionsMenu" ); await page.goto("/notebook", { waitUntil: "domcontentloaded" }); await settle(page); - await page.evaluate(() => { - (window as any).__opened = []; - window.open = (url?: string | URL) => { - (window as any).__opened.push(String(url)); - return null; - }; - }); - await page.getByRole("button", { name: "Launch notebook" }).first().click(); - const dialog = page.getByRole("dialog"); - await expect(dialog.getByRole("radio", { name: "Google Colab" })).toBeChecked(); - await expect(dialog.getByRole("radio", { name: "Private" })).toBeVisible(); - await expect(page).toHaveScreenshot("launch-open.png", { - maxDiffPixelRatio: 0.01, - animations: "disabled", - }); - await dialog.getByRole("button", { name: "Launch Notebook" }).click(); - const opened = await page.evaluate(() => (window as any).__opened); - expect(opened).toHaveLength(1); - expect(opened[0]).toMatch( + const launch = page.getByRole("link", { name: "Launch notebook" }).first(); + await expect(launch).toBeVisible(); + await expect(launch).toHaveAttribute("target", "_blank"); + await expect(launch).toHaveAttribute( + "href", /^https:\/\/colab\.research\.google\.com\/github\/QuantEcon\/[\w.-]+\.notebooks\/blob\/main\/notebook\.ipynb$/ ); });