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

🪐 Launch binder for article theme #239

Merged
merged 22 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
60dbf47
⏫ add launch button to sub pages
stevejpurves Sep 28, 2023
b3c6c54
🎚optionally load thebelite
stevejpurves Sep 28, 2023
13ca796
simple launch, independent binders
stevejpurves Sep 28, 2023
ff92417
launch from supporting docs
stevejpurves Sep 28, 2023
b1248c3
style tweaks
stevejpurves Sep 28, 2023
4f93c2f
🔧 fix react type issue
stevejpurves Sep 28, 2023
39dd855
🍕 consume latest `thebe` changes
stevejpurves Sep 28, 2023
2c65a0c
🚛 move launch binder button
stevejpurves Sep 28, 2023
c51cec8
🛠 fixing pageloader to include recently added `location` field
stevejpurves Sep 28, 2023
2621a16
🚀 launch to a specific name
stevejpurves Sep 28, 2023
4f939be
🔌 binder connection and launch with location
stevejpurves Sep 28, 2023
078cc56
remove debug logging
stevejpurves Sep 28, 2023
4beb59a
auto open binder link after server ready
stevejpurves Sep 28, 2023
f9d7aba
⚠️ show connection error
stevejpurves Sep 28, 2023
37e8b0f
👮🏻enable full override of theeb configuration by a parent theme
stevejpurves Sep 28, 2023
f5e9475
🛟 get connection status tray on top
stevejpurves Sep 28, 2023
969ca76
🗑 remove binder badge option
stevejpurves Sep 28, 2023
25075ac
👩🏾‍🍳 dont bake in custom providers
stevejpurves Sep 28, 2023
b5ba95d
add ErrorTray, NotebookToolbar and placeholder for options
stevejpurves Sep 28, 2023
01f6f67
👊🏽 thebe
stevejpurves Sep 28, 2023
5b7f149
👊🏽👊🏽 thebe
stevejpurves Sep 28, 2023
4049f9c
Merge branch 'main' into feat/binder-for-articles
rowanc1 Oct 3, 2023
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
2 changes: 1 addition & 1 deletion packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type FooterLinks = {

export type PageLoader = {
kind: SourceFileKind;
file: string;
location: string;
sha256: string;
slug: string;
domain: string; // This is written in at render time in the site
Expand Down
47 changes: 23 additions & 24 deletions packages/jupyter/src/ConnectionStatusTray.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { useEffect, useState } from 'react';
import { useThebeServer } from 'thebe-react';
import { useComputeOptions } from './providers';
import { useThebeOptions } from './providers';
import type { ThebeEventData, ThebeEventType } from 'thebe-core';
import { selectAreExecutionScopesBuilding, useExecutionScope } from './execute';

export function ConnectionStatusTray() {
const { thebe } = useComputeOptions();
export function ConnectionStatusTray({ waitForSessions }: { waitForSessions?: boolean }) {
const { options } = useThebeOptions();
const { connecting, ready: serverReady, error: serverError, events } = useThebeServer();
const { slug, ready: scopeReady, state } = useExecutionScope();
const [show, setShow] = useState(false);
const [unsub, setUnsub] = useState<() => void | undefined>();
const [status, setStatus] = useState<string>('[client] Connecting...');

const error = serverError; // TODO scope bulding error handling || sessionError;
const ready = serverReady && scopeReady;
const ready = serverReady && (!waitForSessions || scopeReady);
const busy = connecting || selectAreExecutionScopesBuilding(state, slug);

const handleStatus = (event: any, data: ThebeEventData) => {
Expand All @@ -26,29 +26,28 @@ export function ConnectionStatusTray() {
}, [events]);

useEffect(() => {
if (!thebe) return;
if (thebe?.useBinder || thebe?.useJupyterLite) {
if (busy || error) {
setShow(true);
} else if (ready) {
setTimeout(
() => {
setShow(false);
unsub?.();
setUnsub(undefined);
},
thebe?.useJupyterLite ? 3000 : 500,
);
}
if (!options) return;
if (busy || error) {
setShow(true);
} else if (ready) {
setTimeout(() => {
setShow(false);
unsub?.();
setUnsub(undefined);
}, 1000);
}
}, [thebe, busy, ready, error]);
}, [options, busy, ready, error]);

const host = thebe?.useBinder ? 'Binder' : thebe?.useJupyterLite ? 'JupyterLite' : 'Local Server';
const host = options?.useBinder
? 'Binder'
: options?.useJupyterLite
? 'JupyterLite'
: 'Local Server';

// TODO radix ui toast!
if (show && error) {
return (
<div className="fixed p-3 text-sm text-gray-700 bg-white border rounded shadow-lg bottom-2 sm:right-2 max-w-[90%] md:max-w-[300px] min-w-0">
<div className="fixed p-3 z-[11] text-sm text-gray-700 bg-white border rounded shadow-lg bottom-2 sm:right-2 max-w-[90%] md:max-w-[300px] min-w-0">
<div className="mb-2 font-semibold text-center">⛔️ Error connecting to {host} ⛔️</div>
<div className="my-1 max-h-[15rem] mono overflow-hidden text-ellipsis">{error}</div>
<div className="flex justify-end">
Expand All @@ -64,9 +63,9 @@ export function ConnectionStatusTray() {
);
}

if (show && thebe?.useJupyterLite) {
if (show && options?.useJupyterLite) {
return (
<div className="fixed p-3 text-sm text-gray-700 bg-white border rounded shadow-lg bottom-2 sm:right-2 max-w-[90%] md:max-w-[300px] min-w-0">
<div className="fixed p-3 z-[11] text-sm text-gray-700 bg-white border rounded shadow-lg bottom-2 sm:right-2 max-w-[90%] md:max-w-[300px] min-w-0">
<div className="mb-1 font-semibold text-center">⚡️ Connecting to {host} ⚡️</div>
{!ready && <div className="max-h-[5rem] mono overflow-hidden text-ellipsis">{status}</div>}
{ready && (
Expand All @@ -80,7 +79,7 @@ export function ConnectionStatusTray() {

if (show) {
return (
<div className="fixed p-3 text-sm text-gray-700 bg-white border rounded shadow-lg bottom-2 sm:right-2 max-w-[90%] md:max-w-[300px] min-w-0">
<div className="fixed p-3 z-[11] text-sm text-gray-700 bg-white border rounded shadow-lg bottom-2 sm:right-2 max-w-[90%] md:max-w-[300px] min-w-0">
<div className="mb-1 font-semibold text-center">⚡️ Connecting to {host} ⚡️</div>
<div className="max-h-[15rem] mono overflow-hidden text-ellipsis">{status}</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions packages/jupyter/src/controls/ArticleCellControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export function ArticleStatusBadge({ id }: { id: string }) {
const building = selectAreExecutionScopesBuilding(state, slug);

const handleStart = () => {
if (!connect) {
console.debug("ArticleStatusBadge: Trying to start a connection but connect() isn't defined");
return;
}
connect();
start();
};
Expand Down
134 changes: 134 additions & 0 deletions packages/jupyter/src/controls/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,142 @@ import ArrowUturnLeft from '@heroicons/react/24/outline/ArrowUturnLeftIcon';
import Bolt from '@heroicons/react/24/outline/BoltIcon';
import PowerIcon from '@heroicons/react/24/outline/PowerIcon';
import BoltIconSolid from '@heroicons/react/24/solid/BoltIcon';
import ExclamationCircleIcon from '@heroicons/react/24/outline/ExclamationCircleIcon';
import classNames from 'classnames';
import { Spinner } from './Spinner';
import { useThebeServer } from 'thebe-react';
import { useCallback, useState } from 'react';

function BinderButton({
icon,
label,
title,
busy,
error,
className,
onClick,
}: {
icon: React.ReactNode;
label: string;
title: string;
onClick: (e: React.UIEvent) => void;
busy?: boolean;
error?: boolean;
className?: string;
}) {
let iconToShow = icon;
if (error) {
iconToShow = (
<ExclamationCircleIcon
className="inline pr-2 text-red-600 text-semibold"
width="1.5rem"
height="1.5rem"
title={title}
/>
);
} else if (busy) {
iconToShow = <Spinner size={16} />;
}

return (
<button className={className} disabled={busy} onClick={onClick} title={title}>
<div className="flex items-center h-full">
{iconToShow}
<span>{label}</span>
</div>
</button>
);
}

export function LaunchBinder({ style, location }: { style: 'link' | 'button'; location?: string }) {
const { connect, connecting, ready, server, error } = useThebeServer();
const [autoOpen, setAutoOpen] = useState(false);

// automatically click the link when the server is ready
// but only if the connection was initiated in this component by the user
const autoClick = useCallback(
(node: HTMLAnchorElement) => {
if (node != null && autoOpen) {
node.click();
}
},
[autoOpen],
);

let btnStyles =
'flex gap-1 px-2 py-1 font-normal no-underline border rounded bg-slate-200 border-slate-600 hover:bg-slate-800 hover:text-white hover:border-transparent';
let icon = (
<ArrowTopRightOnSquareIcon
width="1rem"
height="1rem"
className="self-center mr-2 transition-transform group-hover:-translate-x-1 shrink-0"
/>
);
if (style === 'link') {
icon = <ArrowTopRightOnSquareIcon width="1.5rem" height="1.5rem" className="inline h-5 pr-2" />;
btnStyles =
'inline-flex items-center mr-2 font-medium no-underline text-gray-900 lg:mr-0 lg:flex';
}

const handleStart = () => {
if (!connect) {
console.debug("LaunchBinder: Trying to start a connection but connect() isn't defined");
return;
}
setAutoOpen(true);
connect();
};

if (ready) {
// we expect ?token= to be in the url
let userServerUrl = server?.userServerUrl;
if (userServerUrl && location) {
// add the location to the url pathname
const url = new URL(userServerUrl);
if (url.pathname.endsWith('/')) url.pathname = url.pathname.slice(0, -1);
url.pathname = `${url.pathname}/lab/tree${location}`;
userServerUrl = url.toString();
}

return (
<a
ref={autoClick}
className={btnStyles}
href={userServerUrl}
target="_blank"
rel="noopener noreferrer"
title="Binder server is available, click to open in a new tab"
>
<div className="flex items-center h-full">
{icon}
<span>Open in Binder</span>
</div>
</a>
);
}

let label = 'Launch Binder';
let title = 'Click to start a new compute session';
if (error) {
label = 'Launch Error';
title = error;
} else if (connecting) {
label = 'Launching...';
title = 'Connecting to binder, please wait';
}

return (
<BinderButton
className={btnStyles}
icon={icon}
label={label}
title={title}
busy={connecting}
error={!!error}
onClick={handleStart}
/>
);
}

export function SpinnerStatusButton({
ready,
Expand Down
6 changes: 5 additions & 1 deletion packages/jupyter/src/controls/NotebookToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export function NotebookToolbar({ showLaunch = false }: { showLaunch?: boolean }
const { connecting, connect, ready: serverReady, server, error: serverError } = useThebeServer();
const computable = selectIsComputable(state, slug);
const handleStart = () => {
if (!connect) {
console.debug("NotebookToolbar: Trying to start a connection but connect() isn't defined");
return;
}
connect();
start(slug);
};
Expand All @@ -42,7 +46,7 @@ export function NotebookToolbar({ showLaunch = false }: { showLaunch?: boolean }
if (computable)
return (
<div className="sticky top-[60px] flex justify-end w-full z-20 pointer-events-none">
<div className="flex p-1 m-1 border rounded-full shadow pointer-events-auto space-x-1 border-stone-300 bg-white/80 dark:bg-stone-900/80 backdrop-blur">
<div className="flex p-1 m-1 space-x-1 border rounded-full shadow pointer-events-auto border-stone-300 bg-white/80 dark:bg-stone-900/80 backdrop-blur">
{!ready && (
<div className="rounded">
<button
Expand Down
Loading