Skip to content

Commit

Permalink
feature/thebe (#560)
Browse files Browse the repository at this point in the history
* add thebe deps

* add URL param to switch article from v2 to v3 (thebe) 

* fix Slider when missing autoplaySpeed props in Holme page :)

* update article component with url from notebook viewer form component

* manage thebe execution state via zustand

* pre-cell error reporting

* Errortray on top

* fixes to runAll

* add react codemirror

* add updateCellSource function in ExecutionScope state

* Create ArticleCellEditor.js

* updateCellSource on codemirror change event

* add source

* fix binder comment

* no computed objects

* extended development envvar usage

* expose execution count

* envvar for binder is a string

* fix cell errors

* editing with persistent state

* added function to recover binder url

* top level connection status

* ui tweaks

* added clear saved sessions example

* open in jupyter example

* deriving path from url

* bump thebe

* better connection status and error examples

* fix binderOptions and update comment on kernelName for R

* ui tweak

* 🎚enable clean shutdown

* bump thebe-react

* curvenote binder

* trying to fixing dependency issue

* Add default varialble in .env file and change console output

* fixed shutdown callback

* Update WindowEvents.js

* use only env variables to set up ArticleThebeProvider

---------

Co-authored-by: stevejpurves <[email protected]>
  • Loading branch information
danieleguido and stevejpurves authored Mar 18, 2024
1 parent 77cc7fd commit 55cc735
Show file tree
Hide file tree
Showing 20 changed files with 9,961 additions and 4,574 deletions.
8 changes: 7 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ REACT_APP_TAG_EDITORIAL=editorial
REACT_APP_ENABLE_AUTH_0=false
REACT_APP_TWITTER=Journal_DigHist
REACT_APP_FACEBOOK=journalofdigitalhistory
REACT_APP_THEBE_TOKEN=********
REACT_APP_THEBE_DEV_BINDER=false
REACT_APP_THEBE_JUPYTER_URL=http://localhost:8888
REACT_APP_THEBE_BINDER_URL=https://mybinder.org
REACT_APP_GITHUB=https://github.com/C2DH/journal-of-digital-history
REACT_APP_GITHUB_RELEASES_API_ENDPOINT=https://api.github.com/repos/c2dh/journal-of-digital-history/releases

Expand All @@ -32,4 +36,6 @@ REACT_APP_MATOMO_SITEID=1
REACT_APP_MATOMO_URLBASE=https://journalofdigitalhistory.matomo.cloud/
REACT_APP_WIKI_GUIDELINES=/proxy-githubusercontent/wiki/c2dh/journal-of-digital-history/Guidelines.md
REACT_APP_LAUNCH_BINDER_BADGE_URL="https://img.shields.io/badge/-binder-579ACA.svg?logo=data:image/png;base64,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"
REACT_APP_STORYBOOK_BASE_64_PNG_SRC="iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII"
REACT_APP_STORYBOOK_BASE_64_PNG_SRC="iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII"

GENERATE_SOURCEMAP=false
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ node_modules
.development.env
.env.development
.vscode/settings.json
.yalc/*
yalc.lock
storybook-static
# _redirects
journal-of-digital-history.code-workspace
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@auth0/auth0-react": "^1.1.0",
"@c2dh/react-facets": "^1.1.0",
"@curvenote/ansi-to-react": "^7.0.0",
"@gerhobbelt/markdown-it-attrs": "^3.0.3-20",
"@jonkoops/matomo-tracker-react": "^0.7.0",
"@react-spring/web": "^9.7.3",
Expand All @@ -25,6 +26,7 @@
"axios": "^1.6.7",
"bootstrap": "^5.2.3",
"citation-js": "0.6.4",
"codemirror": "5",
"d3-array": "^2.9.1",
"d3-scale": "^3.2.3",
"deepdash-es": "^5.3.0",
Expand All @@ -51,6 +53,7 @@
"query-string": "^7.0.1",
"react": "^18.2.0",
"react-bootstrap": "^2.7.2",
"react-codemirror2": "^7.3.0",
"react-copy-to-clipboard": "^5.0.4",
"react-device-detect": "^1.14.0",
"react-dom": "^18.2.0",
Expand All @@ -70,6 +73,8 @@
"react-window": "^1.8.5",
"sass": "^1.39.2",
"source-map-explorer": "^2.4.2",
"thebe-core": "^0.4.2",
"thebe-react": "^0.4.2",
"typescript": "^3.9.7",
"universal-cookie": "^4.0.4",
"use-query-params": "^1.2.2",
Expand Down Expand Up @@ -124,6 +129,9 @@
"storybook-react-i18next": "^1.1.2",
"webpack": "5"
},
"resolutions": {
"@lumino/algorithm": "2.0.1"
},
"eslintConfig": {
"overrides": [
{
Expand Down
94 changes: 94 additions & 0 deletions src/components/ArticleV3/Article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { useEffect } from 'react'
import { Container } from 'react-bootstrap'
import { ArticleThebeProvider, useArticleThebe } from './ArticleThebeProvider'
import SimpleArticleCell from './SimpleArticleCell'
import { useNotebook } from './hooks'
import ConnectionErrorBox from './ConnectionErrorBox'

import ArticleExecuteToolbar from './ArticleExecuteToolbar'
import { useExecutionScope } from './ExecutionScope'

const Article = ({ url = '', paragraphs }) => {
const { starting, connectionErrors, ready, connectAndStart, restart, session, openInJupyter } =
useArticleThebe()

useEffect(() => {
if (!connectionErrors) return
// if there is a connection error, we want to ensure that the compute UI is
// disabled or in an approprate state - set a disabled flag in the execution state?
}, [connectionErrors])

const attachSession = useExecutionScope((state) => state.attachSession)

useEffect(() => {
if (!ready) return
attachSession(session)
}, [ready])

console.debug('[Article]', url, 'is rendering')

return (
<Container>
<div style={{ paddingTop: 120 }}></div>

<ArticleExecuteToolbar
starting={starting}
ready={ready}
connectAndStart={connectAndStart}
restart={restart}
openInJupyter={openInJupyter}
/>
<ConnectionErrorBox />
{paragraphs.map((cell, idx) => {
return (
<React.Fragment key={[url, idx].join('-')}>
<a className="ArticleLayer_anchor"></a>
<div
className="ArticleLayer_paragraphWrapper"
data-cell-idx={cell.idx}
data-cell-layer={cell.layer}
>
<div className={`ArticleLayer_cellActive off`} />
<SimpleArticleCell
isJavascriptTrusted={false}
onNumClick={() => ({})}
memoid={[url, idx].join('-')}
{...cell}
num={cell.num}
idx={cell.idx}
role={cell.role}
layer={cell.layer}
source={cell.source}
headingLevel={cell.isHeading ? cell.heading.level : 0}
windowHeight={800}
ready={ready}
/>
</div>
</React.Fragment>
)
})}
</Container>
)
}

function ArticleWithContent({ url, ipynb }) {
const { paragraphs, executables } = useNotebook(url, ipynb)

const initExecutionScope = useExecutionScope((state) => state.initialise)

useEffect(() => {
initExecutionScope(executables)
}, [executables, initExecutionScope])

return <Article url={url} paragraphs={paragraphs} />
}

function ThebeArticle({ url = '', ipynb = { cells: [], metadata: {} }, ...props }) {
return (
<ArticleThebeProvider url={url} binderUrl={props.binderUrl}>
<ArticleWithContent url={url} ipynb={ipynb} />
</ArticleThebeProvider>
)
}

export default ThebeArticle
42 changes: 42 additions & 0 deletions src/components/ArticleV3/ArticleCellEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import { Controlled as CodeMirror } from 'react-codemirror2'
import { useExecutionScope } from './ExecutionScope'
// import codemirror style
import 'codemirror/lib/codemirror.css'
// import codemirror dracula style
import 'codemirror/theme/dracula.css'

const ArticleCellEditor = ({ cellIdx = -1, options }) => {
const source = useExecutionScope((state) => state.cells[cellIdx]?.source) ?? ''
const [value, setValue] = React.useState(source)

const updateCellSource = useExecutionScope((state) => state.updateCellSource)
const onCellChangeHandler = (value) => {
console.debug('[ArticleCell] onCellChangeHandler', cellIdx, { value })
updateCellSource(cellIdx, value)
}

return (
<CodeMirror
value={value}
options={{
theme: 'dracula',
mode: 'python',
lineNumbers: true,
lineWrapping: true,
styleActiveLine: true,
matchBrackets: true,
...options,
}}
onBeforeChange={(editor, data, value) => {
setValue(value)
}}
onChange={(editor, data, value) => {
console.debug('[ArticleCellEditor]', cellIdx, { value })
onCellChangeHandler(value)
}}
/>
)
}

export default ArticleCellEditor
26 changes: 26 additions & 0 deletions src/components/ArticleV3/ArticleCellError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import ErrorContent from './ErrorContent'

const ArticleCellError = ({ idx, errors, hideLabel = false }) => {
const outputTypeClassName = `ArticleCellOutput_${errors[0].output_type}`
const { t } = useTranslation()

return (
<blockquote
className={`${outputTypeClassName}`}
style={{ backgroundColor: 'pink', borderLeftColor: 'red' }}
>
{hideLabel ? null : (
<div>
<div className="label" style={{ color: 'red' }}>
{t(outputTypeClassName)}
</div>
</div>
)}
<ErrorContent errors={errors} idx={idx} />
</blockquote>
)
}

export default ArticleCellError
9 changes: 9 additions & 0 deletions src/components/ArticleV3/ArticleCellSourceCodeWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import { useExecutionScope } from './ExecutionScope'
import ArticleCellSourceCode from '../Article/ArticleCellSourceCode'

export default function ArticleCellSourceCodeWrapper(props) {
const { cellIdx } = props
const source = useExecutionScope((state) => state.cells[cellIdx]?.source) ?? ''
return <ArticleCellSourceCode content={source} visible language="python" />
}
112 changes: 112 additions & 0 deletions src/components/ArticleV3/ArticleExecuteToolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useCallback } from 'react'
import { useExecutionScope } from './ExecutionScope'
import ConnectionStatusBox from './ConnectionStatusBox'
import { useThebeLoader } from 'thebe-react'
import { useArticleThebe } from './ArticleThebeProvider'

export default function ArticleExecuteToolbar({
starting,
ready,
connectAndStart,
restart,
openInJupyter,
}) {
const { core } = useThebeLoader()
const { shutdown } = useArticleThebe()
const executing = useExecutionScope((state) => state.executing)
const executeAll = useExecutionScope((state) => state.executeAll)
const clearAll = useExecutionScope((state) => state.clearAll)
const resetAll = useExecutionScope((state) => state.resetAll)

const shutdownAndReset = useCallback(() => {
resetAll()
shutdown()
}, [shutdown, resetAll])

const clearSavedSessions = useCallback(() => {
if (!core) return
core.clearAllSavedSessions()
// Note: is is possible to clear the saved session only for this article
// provided yo ucan supply the sotragePrefix and correct (repository) url
// to core.clearSavedSession(storagePrefix, url)
}, [core])

console.log('[ArticleExecuteToolbar]', { starting, ready, executing }, 'rendering')

return (
<div style={{ position: 'sticky', top: 100, zIndex: 10, marginBottom: 12 }}>
{!starting && !ready && (
<>
<button
style={{ margin: '4px', color: 'green' }}
disabled={starting || ready}
onClick={connectAndStart}
>
Start
</button>
<button
style={{ margin: '4px', color: 'green' }}
disabled={starting || ready}
onClick={clearSavedSessions}
title="upon successful connection to binderhub the session connection information is saved in local storage. This button will clear that information and force new servers to be started."
>
Clear Saved Sessions
</button>
</>
)}
{starting && (
<span
style={{
display: 'inline-block',
padding: 4,
width: '100%',
backgroundColor: 'lightgreen',
}}
>
Starting...
</span>
)}
{ready && (
<div
style={{
display: 'flex',
padding: 4,
backgroundColor: 'lightgreen',
width: '100%',
alignItems: 'center',
gap: 4,
}}
>
{executing ? 'RUNNING...' : 'READY'}
<div style={{ flexGrow: 1 }} />
<button
onClick={() => {
clearAll()
executeAll()
}}
disabled={executing}
>
run all
</button>
<button onClick={clearAll} disabled={executing}>
clear all
</button>
<button onClick={resetAll} disabled={executing}>
reset all
</button>
<button onClick={restart} disabled={executing}>
restart kernel
</button>
{/* TODO: feed notebook name in here if different */}
<button onClick={() => openInJupyter('article.ipynb')} disabled={executing}>
jupyter
</button>
<button onClick={shutdownAndReset} disabled={executing}>
shutdown
</button>
</div>
)}
<ConnectionStatusBox />
</div>
)
}
Loading

0 comments on commit 55cc735

Please sign in to comment.