Skip to content

Commit

Permalink
🌟 Featured notebooks & new main menu (#2048)
Browse files Browse the repository at this point in the history
Co-authored-by: merlijnkersten <[email protected]>
  • Loading branch information
fonsp and merlijnkersten authored Jun 16, 2022
1 parent 5b987fd commit 9787ebb
Show file tree
Hide file tree
Showing 24 changed files with 1,012 additions and 160 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ Lastly, here's _**one more feature**_: Pluto notebooks have a `@bind` macro to c

<br >

You don't need to know HTML to use it! The [PlutoUI package](https://github.com/fonsp/PlutoUI.jl) contains basic inputs like sliders and buttons. Pluto's interactivity is very easy to use, you will learn more from the sample notebooks inside Pluto!
You don't need to know HTML to use it! The [PlutoUI package](https://github.com/fonsp/PlutoUI.jl) contains basic inputs like sliders and buttons. Pluto's interactivity is very easy to use, you will learn more from the featured notebooks inside Pluto!

But for those who want to dive deeper - you can use HTML, JavaScript and CSS to write your own widgets! Custom update events can be fired by dispatching a `new CustomEvent("input")`, making it compatible with the [`viewof` operator of observablehq](https://observablehq.com/@observablehq/a-brief-introduction-to-viewof). Have a look at the JavaScript sample notebook inside Pluto!
But for those who want to dive deeper - you can use HTML, JavaScript and CSS to write your own widgets! Custom update events can be fired by dispatching a `new CustomEvent("input")`, making it compatible with the [`viewof` operator of observablehq](https://observablehq.com/@observablehq/a-brief-introduction-to-viewof). Have a look at the JavaScript featured notebook inside Pluto!

<br >

Expand Down Expand Up @@ -160,9 +160,9 @@ Pluto.jl is open source! Specifically, it is [MIT Licensed](https://github.com/f

If you want to reference Pluto.jl in scientific writing, you can use our DOI: [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4792401.svg)](https://doi.org/10.5281/zenodo.4792401)

### Sample notebooks
### Featured notebooks

The included sample notebooks have a more permissive license: the [Unlicense](https://github.com/fonsp/Pluto.jl/blob/main/sample/LICENSE). This means that you can use sample notebook code however you like - you do not need to credit us!
Unless otherwise specified, the included featured notebooks have a more permissive license: the [Unlicense](https://github.com/fonsp/Pluto.jl/blob/main/sample/LICENSE). This means that you can use them however you like - you do not need to credit us!

Your notebook files are _yours_, you also do not need to credit us. Have fun!

Expand Down
4 changes: 2 additions & 2 deletions frontend-bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"version": "1.0.0",
"description": "",
"scripts": {
"start": "cd ../frontend && parcel --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html sample.html",
"build": "cd ../frontend && parcel build --no-source-maps --public-url . --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html sample.html && node ../frontend-bundler/add_sri.js ../frontend-dist/editor.html",
"start": "cd ../frontend && parcel --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html",
"build": "cd ../frontend && parcel build --no-source-maps --public-url . --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html && node ../frontend-bundler/add_sri.js ../frontend-dist/editor.html",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
Expand Down
21 changes: 17 additions & 4 deletions frontend/common/Binder.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,17 @@ export const start_binder = async ({ setStatePromise, connect, launch_params })
let open_response = new Response()

if (launch_params.notebookfile.startsWith("data:")) {
open_response = await fetch(with_token(new URL("notebookupload", binder_session_url)), {
method: "POST",
body: await (await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity }))).arrayBuffer(),
})
open_response = await fetch(
with_token(
with_query_params(new URL("notebookupload", binder_session_url), {
name: new URLSearchParams(window.location.search).get("name"),
})
),
{
method: "POST",
body: await (await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity }))).arrayBuffer(),
}
)
} else {
for (const [p1, p2] of [
["path", launch_params.notebookfile],
Expand All @@ -145,6 +152,12 @@ export const start_binder = async ({ setStatePromise, connect, launch_params })
}
}

if (!open_response.ok) {
let b = await open_response.blob()
window.location.href = URL.createObjectURL(b)
return
}

// Opening a notebook gives us the notebook ID, which means that we have a running session! Time to connect.

const new_notebook_id = await open_response.text()
Expand Down
3 changes: 2 additions & 1 deletion frontend/common/Bond.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const set_bound_elements_to_their_value = (bond_nodes, bond_values) => {
export const add_bonds_disabled_message_handler = (bond_nodes, invalidation) => {
bond_nodes.forEach((bond_node) => {
const listener = (e) => {
if (e.target.closest(".bonds_disabled.offer_binder")) {
if (e.target.closest(".bonds_disabled:where(.offer_binder, .offer_local)")) {
open_pluto_popup({
type: "info",
source_element: e.target,
Expand All @@ -163,6 +163,7 @@ export const add_bonds_disabled_message_handler = (bond_nodes, invalidation) =>
//@ts-ignore
window.open_edit_or_run_popup()
e.preventDefault()
window.dispatchEvent(new CustomEvent("close pluto popup"))
}}
>Run this notebook</a
>
Expand Down
80 changes: 80 additions & 0 deletions frontend/common/RunLocal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import immer from "../imports/immer.js"
import { BackendLaunchPhase } from "./Binder.js"
import { timeout_promise } from "./PlutoConnection.js"
import { with_query_params } from "./URLTools.js"

// This file is very similar to `start_binder` in Binder.js

/**
*
* @param {{
* launch_params: import("../components/Editor.js").LaunchParameters,
* setStatePromise: any,
* connect: () => Promise<void>,
* }} props
*/
export const start_local = async ({ setStatePromise, connect, launch_params }) => {
try {
if (launch_params.pluto_server_url == null || launch_params.notebookfile == null) throw Error("Invalid launch parameters for starting locally.")

await setStatePromise(
immer((state) => {
state.backend_launch_phase = BackendLaunchPhase.created
state.disable_ui = false
})
)

const with_token = (x) => String(x)
const binder_session_url = new URL(launch_params.pluto_server_url, window.location.href)

let open_response

// We download the notebook file contents, and then upload them to the Pluto server.
const notebook_contents = await (
await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity ?? undefined }))
).arrayBuffer()

open_response = await fetch(
with_token(
with_query_params(new URL("notebookupload", binder_session_url), {
name: new URLSearchParams(window.location.search).get("name"),
clear_frontmatter: "yesplease",
})
),
{
method: "POST",
body: notebook_contents,
}
)

if (!open_response.ok) {
let b = await open_response.blob()
window.location.href = URL.createObjectURL(b)
return
}

const new_notebook_id = await open_response.text()
const edit_url = with_query_params(new URL("edit", binder_session_url), { id: new_notebook_id })
console.info("notebook_id:", new_notebook_id)

window.history.replaceState({}, "", edit_url)

await setStatePromise(
immer((state) => {
state.notebook.notebook_id = new_notebook_id
state.backend_launch_phase = BackendLaunchPhase.notebook_running
})
)
console.log("Connecting WebSocket")

const connect_promise = connect()
await timeout_promise(connect_promise, 20_000).catch((e) => {
console.error("Failed to establish connection within 20 seconds. Navigating to the edit URL directly.", e)

window.parent.location.href = with_token(edit_url)
})
} catch (err) {
console.error("Failed to initialize binder!", err)
alert("Something went wrong! 😮\n\nWe failed to open this notebook. Please try again with a different browser, or come back later.")
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import { BackendLaunchPhase } from "../common/Binder.js"
import { html, useEffect, useState, useRef } from "../imports/Preact.js"

export const RunLocalButton = ({ show, start_local }) => {
//@ts-ignore
window.open_edit_or_run_popup = () => {
start_local()
}

return html`<div class="edit_or_run">
<button
onClick=${(e) => {
e.stopPropagation()
e.preventDefault()
start_local()
}}
>
<b>Edit</b> or <b>run</b> this notebook
</button>
</div>`
}

export const BinderButton = ({ offer_binder, start_binder, notebookfile }) => {
const [popupOpen, setPopupOpen] = useState(false)
const [showCopyPopup, setShowCopyPopup] = useState(false)
Expand Down
32 changes: 27 additions & 5 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea
import { PlutoActionsContext, PlutoBondsContext, PlutoJSInitializingContext, SetWithEmptyCallback } from "../common/PlutoContext.js"
import { start_binder, BackendLaunchPhase, count_stat } from "../common/Binder.js"
import { setup_mathjax } from "../common/SetupMathJax.js"
import { BinderButton } from "./BinderButton.js"
import { BinderButton, RunLocalButton } from "./EditOrRunButton.js"
import { slider_server_actions, nothing_actions } from "../common/SliderServerClient.js"
import { ProgressBar } from "./ProgressBar.js"
import { IsolatedCell } from "./Cell.js"
import { RawHTMLContainer } from "./CellOutput.js"
import { RecordingPlaybackUI, RecordingUI } from "./RecordingUI.js"
import { HijackExternalLinksToOpenInNewTab } from "./HackySideStuff/HijackExternalLinksToOpenInNewTab.js"
import { start_local } from "../common/RunLocal.js"
import { FrontMatterInput } from "./FrontmatterInput.js"

// This is imported asynchronously - uncomment for development
// import environment from "../common/Environment.js"

export const default_path = "..."
export const default_path = ""
const DEBUG_DIFFING = false

// Be sure to keep this in sync with DEFAULT_CELL_METADATA in Cell.jl
Expand Down Expand Up @@ -88,6 +89,7 @@ const statusmap = (/** @type {EditorState} */ state, /** @type {LaunchParameters
static_preview: state.static_preview,
bonds_disabled: !(state.connected || state.initializing || launch_params.slider_server_url != null),
offer_binder: state.backend_launch_phase === BackendLaunchPhase.wait_for_user && launch_params.binder_url != null,
offer_local: state.backend_launch_phase === BackendLaunchPhase.wait_for_user && launch_params.pluto_server_url != null,
binder: launch_params.binder_url != null && state.backend_launch_phase != null,
code_differs: state.notebook.cell_order.some(
(cell_id) => state.cell_inputs_local[cell_id] != null && state.notebook.cell_inputs[cell_id].code !== state.cell_inputs_local[cell_id].code
Expand Down Expand Up @@ -191,6 +193,7 @@ const first_true_key = (obj) => {
* preamble_html: string?,
* isolated_cell_ids: string[]?,
* binder_url: string?,
* pluto_server_url: string?,
* slider_server_url: string?,
* recording_url: string?,
* recording_url_integrity: string?,
Expand Down Expand Up @@ -288,7 +291,10 @@ export class Editor extends Component {

disable_ui: launch_params.disable_ui,
static_preview: launch_params.statefile != null,
backend_launch_phase: launch_params.notebookfile != null && launch_params.binder_url != null ? BackendLaunchPhase.wait_for_user : null,
backend_launch_phase:
launch_params.notebookfile != null && (launch_params.binder_url != null || launch_params.pluto_server_url != null)
? BackendLaunchPhase.wait_for_user
: null,
binder_session_url: null,
binder_session_token: null,
connected: false,
Expand Down Expand Up @@ -1212,7 +1218,11 @@ patch: ${JSON.stringify(
initializing: false,
})
// view stats on https://stats.plutojl.org/
count_stat(`article-view`)
if (this.state.pluto_server_url != null) {
count_stat(`article-view`)
} else {
count_stat(`article-view`)
}
} else {
this.connect()
}
Expand Down Expand Up @@ -1315,6 +1325,9 @@ patch: ${JSON.stringify(
<${PlutoActionsContext.Provider} value=${this.actions}>
<${PlutoBondsContext.Provider} value=${this.state.notebook.bonds}>
<${PlutoJSInitializingContext.Provider} value=${this.js_init_set}>
<button title="Go back" onClick=${() => {
history.back()
}} class="floating_back_button"><span></span></button>
<${Scroller} active=${this.state.scroller} />
<${ProgressBar} notebook=${this.state.notebook} backend_launch_phase=${this.state.backend_launch_phase} status=${status}/>
<header id="pluto-nav" className=${export_menu_open ? "show_export" : ""}>
Expand Down Expand Up @@ -1408,7 +1421,16 @@ patch: ${JSON.stringify(
/>
${
status.offer_binder
status.offer_local
? html`<${RunLocalButton}
start_local=${() =>
start_local({
setStatePromise: this.setStatePromise,
connect: this.connect,
launch_params: launch_params,
})}
/>`
: status.offer_binder
? html`<${BinderButton}
offer_binder=${status.offer_binder}
start_binder=${() =>
Expand Down
10 changes: 8 additions & 2 deletions frontend/components/Popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,32 @@ export const Popup = ({ notebook }) => {
set_recent_event(e.detail)
}

const close = () => {
set_recent_event(null)
}

useEffect(() => {
const onpointerdown = (e) => {
if (e.target == null) return
if (e.target.closest("pluto-popup") != null) return
if (recent_source_element_ref.current == null) return
if (recent_source_element_ref.current.contains(e.target)) return

set_recent_event(null)
close()
}
const onkeydown = (e) => {
if (e.key === "Escape") {
set_recent_event(null)
close()
}
}
window.addEventListener("open pluto popup", open)
window.addEventListener("close pluto popup", close)
window.addEventListener("pointerdown", onpointerdown)
document.addEventListener("keydown", onkeydown)

return () => {
window.removeEventListener("open pluto popup", open)
window.removeEventListener("close pluto popup", close)
window.removeEventListener("pointerdown", onpointerdown)
document.removeEventListener("keydown", onkeydown)
}
Expand Down
Loading

0 comments on commit 9787ebb

Please sign in to comment.