+
@@ -695,120 +732,120 @@ Summary
Only the instructions. Make sure you are installing as the root user. I assume you have Python and conda installed.
Create the conda environment
-
sudo -i
-
-conda create -n jupyterhub python --yes
-conda activate jupyterhub
-conda install -c conda-forge jupyterhub --yes
-conda install -c conda-forge jupyterlab notebook --yes
-
-JHUBENV=/opt/miniconda3/envs/jupyterhub
-chmod 755 $JHUBENV
+
sudo -i
+
+conda create -n jupyterhub python --yes
+conda activate jupyterhub
+conda install -c conda-forge jupyterhub --yes
+conda install -c conda-forge jupyterlab notebook --yes
+
+JHUBENV=/opt/miniconda3/envs/jupyterhub
+chmod 755 $JHUBENV
Create user
Open the 8000 port for access to the application.
-
#sudo systemctl enable firewalld
-#sudo systemctl start firewalld
-
-sudo firewall-cmd --permanent --add-port 8000/tcp
-sudo firewall-cmd --reload
-sudo firewall-cmd --list-ports
+
#sudo systemctl enable firewalld
+#sudo systemctl start firewalld
+
+sudo firewall-cmd --permanent --add-port 8000/tcp
+sudo firewall-cmd --reload
+sudo firewall-cmd --list-ports
Create the configuration file. Will be edited at end.
-
sudo mkdir -p $JHUBENV/etc/jupyterhub/
-cd $JHUBENV/etc/jupyterhub/
-sudo $JHUBENV/bin/jupyterhub --generate-config
+
sudo mkdir -p $JHUBENV/etc/jupyterhub/
+cd $JHUBENV/etc/jupyterhub/
+sudo $JHUBENV/bin/jupyterhub --generate-config
Install docker if needed
-
sudo yum install -y yum-utils
-sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
-
-sudo systemctl start docker
+
sudo yum install -y yum-utils
+sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
+
+sudo systemctl start docker
Not sure this is needed.
-
sudo firewall-cmd --zone=docker --add-port=8081/tcp
-sudo firewall-cmd --reload
-sudo systemctl restart docker
+
sudo firewall-cmd --zone=docker --add-port=8081/tcp
+sudo firewall-cmd --reload
+sudo systemctl restart docker
Install dockerspawner
-
conda install -c conda-forge dockerspawner --yes
-conda install -c conda-forge docker-py --yes
+
conda install -c conda-forge dockerspawner --yes
+conda install -c conda-forge docker-py --yes
Edit the configuration file.
-
cd $JHUBENV/etc/jupyterhub/
-nano jupyterhub_config.py
+
cd $JHUBENV/etc/jupyterhub/
+nano jupyterhub_config.py
Paste this in
-
# Configuration file for jupyterhub.
-
-c = get_config() #noqa
-c.JupyterHub.port = 8000
-c.JupyterHub.hub_bind_url = "http://0.0.0.0:8081"
-c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
-c.DockerSpawner.remove = True
-c.Spawner.http_timeout = 3600
-c.DockerSpawner.image_whitelist = {
- 'iorocker': 'eeholmes/iorocker-standalone:20231003',
- 'rocker-binder': 'eeholmes/rocker-binder:20231003',
- 'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',
- 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',
- 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',
-}
-
-notebook_dir = '/home/jovyan'
-c.DockerSpawner.notebook_dir = notebook_dir
-
-# Mount the real user's Docker volume on the host to the notebook user's
-# notebook directory in the container
-c.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }
+
# Configuration file for jupyterhub.
+
+c = get_config() #noqa
+c.JupyterHub.port = 8000
+c.JupyterHub.hub_bind_url = "http://0.0.0.0:8081"
+c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
+c.DockerSpawner.remove = True
+c.Spawner.http_timeout = 3600
+c.DockerSpawner.image_whitelist = {
+ 'iorocker': 'eeholmes/iorocker-standalone:20231003',
+ 'rocker-binder': 'eeholmes/rocker-binder:20231003',
+ 'openscapes-rocker': 'eeholmes/minimal-jhub:20231004',
+ 'datascience-r': 'jupyter/datascience-notebook:r-4.3.1',
+ 'scipy-notebook': 'jupyter/scipy-notebook:7e1a19a8427f',
+}
+
+notebook_dir = '/home/jovyan'
+c.DockerSpawner.notebook_dir = notebook_dir
+
+# Mount the real user's Docker volume on the host to the notebook user's
+# notebook directory in the container
+c.DockerSpawner.volumes = { 'jupyter-{username}': notebook_dir }
Docker pull of the images. Do all.
-
docker pull jupyter/datascience-notebook:r-4.3.1
-docker pull jupyter/scipy-notebook:7e1a19a8427f
+
docker pull jupyter/datascience-notebook:r-4.3.1
+docker pull jupyter/scipy-notebook:7e1a19a8427f
Make a new server service
-
sudo mkdir -p $JHUBENV/etc/systemd
-cd $JHUBENV/etc/systemd
-nano jupyterhub.service
+
sudo mkdir -p $JHUBENV/etc/systemd
+cd $JHUBENV/etc/systemd
+nano jupyterhub.service
Paste this in
-
[Unit]
-Description=JupyterHub
-After=syslog.target network.target
-
-[Service]
-User=root
-Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin"
-ExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py
-
-[Install]
-WantedBy=multi-user.target
+
[Unit]
+Description=JupyterHub
+After=syslog.target network.target
+
+[Service]
+User=root
+Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/miniconda3/envs/jupyterhub/bin"
+ExecStart=/opt/miniconda3/envs/jupyterhub/bin/jupyterhub -f /opt/miniconda3/envs/jupyterhub/etc/jupyterhub/jupyterhub_config.py
+
+[Install]
+WantedBy=multi-user.target
Make sure SELinux doesn’t block our service
-
ls -Z $JHUBENV/etc/systemd/
-sudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service
-sudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \;
+
ls -Z $JHUBENV/etc/systemd/
+sudo chcon system_u:object_r:systemd_unit_file_t:s0 $JHUBENV/etc/systemd/jupyterhub.service
+sudo find $JHUBENV/bin -type f -exec chcon system_u:object_r:bin_t:s0 {} \;
Enable our new service
-
sudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service
-sudo systemctl daemon-reload
-sudo systemctl enable jupyterhub.service
-sudo systemctl start jupyterhub.service
+
sudo ln -s $JHUBENV/etc/systemd/jupyterhub.service /etc/systemd/system/jupyterhub.service
+sudo systemctl daemon-reload
+sudo systemctl enable jupyterhub.service
+sudo systemctl start jupyterhub.service
Done! See the long instructions if anything is not working.
Now go through the https and GitHub authentication steps if you need that.
@@ -860,6 +897,33 @@ Summary
}
}
}
+ const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => {
+ const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light';
+ const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark';
+ let newTheme = '';
+ if(darkModeDefault) {
+ newTheme = isAlternate ? baseTheme : alternateTheme;
+ } else {
+ newTheme = isAlternate ? alternateTheme : baseTheme;
+ }
+ const changeGiscusTheme = () => {
+ // From: https://github.com/giscus/giscus/issues/336
+ const sendMessage = (message) => {
+ const iframe = document.querySelector('iframe.giscus-frame');
+ if (!iframe) return;
+ iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app');
+ }
+ sendMessage({
+ setConfig: {
+ theme: newTheme
+ }
+ });
+ }
+ const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null;
+ if (isGiscussLoaded) {
+ changeGiscusTheme();
+ }
+ }
const toggleColorMode = (alternate) => {
// Switch the stylesheets
const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate');
@@ -926,13 +990,15 @@ Summary
return localAlternateSentinel;
}
}
- let localAlternateSentinel = 'default';
+ const darkModeDefault = false;
+ let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default';
// Dark / light mode switch
window.quartoToggleColorScheme = () => {
// Read the current dark / light value
let toAlternate = !hasAlternateSentinel();
toggleColorMode(toAlternate);
setStyleSentinel(toAlternate);
+ toggleGiscusIfUsed(toAlternate, darkModeDefault);
};
// Ensure there is a toggle, if there isn't float one in the top right
if (window.document.querySelector('.quarto-color-scheme-toggle') === null) {
@@ -1011,10 +1077,9 @@ Summary
// clear code selection
e.clearSelection();
});
- function tippyHover(el, contentFn) {
+ function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) {
const config = {
allowHTML: true,
- content: contentFn,
maxWidth: 500,
delay: 100,
arrow: false,
@@ -1024,8 +1089,17 @@ Summary
interactive: true,
interactiveBorder: 10,
theme: 'quarto',
- placement: 'bottom-start'
+ placement: 'bottom-start',
};
+ if (contentFn) {
+ config.content = contentFn;
+ }
+ if (onTriggerFn) {
+ config.onTrigger = onTriggerFn;
+ }
+ if (onUntriggerFn) {
+ config.onUntrigger = onUntriggerFn;
+ }
window.tippy(el, config);
}
const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]');
@@ -1039,6 +1113,128 @@ Summary
const note = window.document.getElementById(id);
return note.innerHTML;
});
+ }
+ const xrefs = window.document.querySelectorAll('a.quarto-xref');
+ const processXRef = (id, note) => {
+ // Strip column container classes
+ const stripColumnClz = (el) => {
+ el.classList.remove("page-full", "page-columns");
+ if (el.children) {
+ for (const child of el.children) {
+ stripColumnClz(child);
+ }
+ }
+ }
+ stripColumnClz(note)
+ const typesetMath = (el) => {
+ if (window.MathJax) {
+ // MathJax Typeset
+ window.MathJax.typeset([el]);
+ } else if (window.katex) {
+ // KaTeX Render
+ var mathElements = el.getElementsByClassName("math");
+ var macros = [];
+ for (var i = 0; i < mathElements.length; i++) {
+ var texText = mathElements[i].firstChild;
+ if (mathElements[i].tagName == "SPAN") {
+ window.katex.render(texText.data, mathElements[i], {
+ displayMode: mathElements[i].classList.contains('display'),
+ throwOnError: false,
+ macros: macros,
+ fleqn: false
+ });
+ }
+ }
+ }
+ }
+ if (id === null || id.startsWith('sec-')) {
+ // Special case sections, only their first couple elements
+ const container = document.createElement("div");
+ if (note.children && note.children.length > 2) {
+ for (let i = 0; i < 2; i++) {
+ container.appendChild(note.children[i].cloneNode(true));
+ }
+ typesetMath(container);
+ return container.innerHTML
+ } else {
+ typesetMath(note);
+ return note.innerHTML;
+ }
+ } else {
+ // Remove any anchor links if they are present
+ const anchorLink = note.querySelector('a.anchorjs-link');
+ if (anchorLink) {
+ anchorLink.remove();
+ }
+ typesetMath(note);
+ return note.innerHTML;
+ }
+ }
+ for (var i=0; i res.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const htmlDoc = parser.parseFromString(html, "text/html");
+ const note = htmlDoc.getElementById(id);
+ if (note !== null) {
+ const html = processXRef(id, note);
+ instance.setContent(html);
+ }
+ }).finally(() => {
+ instance.enable();
+ instance.show();
+ });
+ }
+ } else {
+ // See if we can fetch a full url (with no hash to target)
+ // This is a special case and we should probably do some content thinning / targeting
+ fetch(url)
+ .then(res => res.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const htmlDoc = parser.parseFromString(html, "text/html");
+ const note = htmlDoc.querySelector('main.content');
+ if (note !== null) {
+ // This should only happen for chapter cross references
+ // (since there is no id in the URL)
+ // remove the first header
+ if (note.children.length > 0 && note.children[0].tagName === "HEADER") {
+ note.children[0].remove();
+ }
+ const html = processXRef(null, note);
+ instance.setContent(html);
+ }
+ }).finally(() => {
+ instance.enable();
+ instance.show();
+ });
+ }
+ }, function(instance) {
+ });
}
let selectedAnnoteEl;
const selectorForAnnotation = ( cell, annotation) => {
@@ -1081,6 +1277,7 @@ Summary
}
div.style.top = top - 2 + "px";
div.style.height = height + 4 + "px";
+ div.style.left = 0;
let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter");
if (gutterDiv === null) {
gutterDiv = window.document.createElement("div");
@@ -1106,6 +1303,32 @@ Summary
});
selectedAnnoteEl = undefined;
};
+ // Handle positioning of the toggle
+ window.addEventListener(
+ "resize",
+ throttle(() => {
+ elRect = undefined;
+ if (selectedAnnoteEl) {
+ selectCodeLines(selectedAnnoteEl);
+ }
+ }, 10)
+ );
+ function throttle(fn, ms) {
+ let throttle = false;
+ let timer;
+ return (...args) => {
+ if(!throttle) { // first call gets through
+ fn.apply(this, args);
+ throttle = true;
+ } else { // all the others get throttled
+ if(timer) clearTimeout(timer); // cancel #2
+ timer = setTimeout(() => {
+ fn.apply(this, args);
+ timer = throttle = false;
+ }, ms);
+ }
+ };
+ }
// Attach click handler to the DT
const annoteDls = window.document.querySelectorAll('dt[data-target-cell]');
for (const annoteDlNode of annoteDls) {
@@ -1167,12 +1390,12 @@ Summary
@@ -572,6 +581,33 @@ S3 access
}
}
}
+ const toggleGiscusIfUsed = (isAlternate, darkModeDefault) => {
+ const baseTheme = document.querySelector('#giscus-base-theme')?.value ?? 'light';
+ const alternateTheme = document.querySelector('#giscus-alt-theme')?.value ?? 'dark';
+ let newTheme = '';
+ if(darkModeDefault) {
+ newTheme = isAlternate ? baseTheme : alternateTheme;
+ } else {
+ newTheme = isAlternate ? alternateTheme : baseTheme;
+ }
+ const changeGiscusTheme = () => {
+ // From: https://github.com/giscus/giscus/issues/336
+ const sendMessage = (message) => {
+ const iframe = document.querySelector('iframe.giscus-frame');
+ if (!iframe) return;
+ iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app');
+ }
+ sendMessage({
+ setConfig: {
+ theme: newTheme
+ }
+ });
+ }
+ const isGiscussLoaded = window.document.querySelector('iframe.giscus-frame') !== null;
+ if (isGiscussLoaded) {
+ changeGiscusTheme();
+ }
+ }
const toggleColorMode = (alternate) => {
// Switch the stylesheets
const alternateStylesheets = window.document.querySelectorAll('link.quarto-color-scheme.quarto-color-alternate');
@@ -638,13 +674,15 @@ S3 access
return localAlternateSentinel;
}
}
- let localAlternateSentinel = 'default';
+ const darkModeDefault = false;
+ let localAlternateSentinel = darkModeDefault ? 'alternate' : 'default';
// Dark / light mode switch
window.quartoToggleColorScheme = () => {
// Read the current dark / light value
let toAlternate = !hasAlternateSentinel();
toggleColorMode(toAlternate);
setStyleSentinel(toAlternate);
+ toggleGiscusIfUsed(toAlternate, darkModeDefault);
};
// Ensure there is a toggle, if there isn't float one in the top right
if (window.document.querySelector('.quarto-color-scheme-toggle') === null) {
@@ -723,10 +761,9 @@ S3 access
// clear code selection
e.clearSelection();
});
- function tippyHover(el, contentFn) {
+ function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) {
const config = {
allowHTML: true,
- content: contentFn,
maxWidth: 500,
delay: 100,
arrow: false,
@@ -736,8 +773,17 @@ S3 access
interactive: true,
interactiveBorder: 10,
theme: 'quarto',
- placement: 'bottom-start'
+ placement: 'bottom-start',
};
+ if (contentFn) {
+ config.content = contentFn;
+ }
+ if (onTriggerFn) {
+ config.onTrigger = onTriggerFn;
+ }
+ if (onUntriggerFn) {
+ config.onUntrigger = onUntriggerFn;
+ }
window.tippy(el, config);
}
const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]');
@@ -751,6 +797,128 @@ S3 access
const note = window.document.getElementById(id);
return note.innerHTML;
});
+ }
+ const xrefs = window.document.querySelectorAll('a.quarto-xref');
+ const processXRef = (id, note) => {
+ // Strip column container classes
+ const stripColumnClz = (el) => {
+ el.classList.remove("page-full", "page-columns");
+ if (el.children) {
+ for (const child of el.children) {
+ stripColumnClz(child);
+ }
+ }
+ }
+ stripColumnClz(note)
+ const typesetMath = (el) => {
+ if (window.MathJax) {
+ // MathJax Typeset
+ window.MathJax.typeset([el]);
+ } else if (window.katex) {
+ // KaTeX Render
+ var mathElements = el.getElementsByClassName("math");
+ var macros = [];
+ for (var i = 0; i < mathElements.length; i++) {
+ var texText = mathElements[i].firstChild;
+ if (mathElements[i].tagName == "SPAN") {
+ window.katex.render(texText.data, mathElements[i], {
+ displayMode: mathElements[i].classList.contains('display'),
+ throwOnError: false,
+ macros: macros,
+ fleqn: false
+ });
+ }
+ }
+ }
+ }
+ if (id === null || id.startsWith('sec-')) {
+ // Special case sections, only their first couple elements
+ const container = document.createElement("div");
+ if (note.children && note.children.length > 2) {
+ for (let i = 0; i < 2; i++) {
+ container.appendChild(note.children[i].cloneNode(true));
+ }
+ typesetMath(container);
+ return container.innerHTML
+ } else {
+ typesetMath(note);
+ return note.innerHTML;
+ }
+ } else {
+ // Remove any anchor links if they are present
+ const anchorLink = note.querySelector('a.anchorjs-link');
+ if (anchorLink) {
+ anchorLink.remove();
+ }
+ typesetMath(note);
+ return note.innerHTML;
+ }
+ }
+ for (var i=0; i res.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const htmlDoc = parser.parseFromString(html, "text/html");
+ const note = htmlDoc.getElementById(id);
+ if (note !== null) {
+ const html = processXRef(id, note);
+ instance.setContent(html);
+ }
+ }).finally(() => {
+ instance.enable();
+ instance.show();
+ });
+ }
+ } else {
+ // See if we can fetch a full url (with no hash to target)
+ // This is a special case and we should probably do some content thinning / targeting
+ fetch(url)
+ .then(res => res.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const htmlDoc = parser.parseFromString(html, "text/html");
+ const note = htmlDoc.querySelector('main.content');
+ if (note !== null) {
+ // This should only happen for chapter cross references
+ // (since there is no id in the URL)
+ // remove the first header
+ if (note.children.length > 0 && note.children[0].tagName === "HEADER") {
+ note.children[0].remove();
+ }
+ const html = processXRef(null, note);
+ instance.setContent(html);
+ }
+ }).finally(() => {
+ instance.enable();
+ instance.show();
+ });
+ }
+ }, function(instance) {
+ });
}
let selectedAnnoteEl;
const selectorForAnnotation = ( cell, annotation) => {
@@ -793,6 +961,7 @@ S3 access
}
div.style.top = top - 2 + "px";
div.style.height = height + 4 + "px";
+ div.style.left = 0;
let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter");
if (gutterDiv === null) {
gutterDiv = window.document.createElement("div");
@@ -818,6 +987,32 @@ S3 access
});
selectedAnnoteEl = undefined;
};
+ // Handle positioning of the toggle
+ window.addEventListener(
+ "resize",
+ throttle(() => {
+ elRect = undefined;
+ if (selectedAnnoteEl) {
+ selectCodeLines(selectedAnnoteEl);
+ }
+ }, 10)
+ );
+ function throttle(fn, ms) {
+ let throttle = false;
+ let timer;
+ return (...args) => {
+ if(!throttle) { // first call gets through
+ fn.apply(this, args);
+ throttle = true;
+ } else { // all the others get throttled
+ if(timer) clearTimeout(timer); // cancel #2
+ timer = setTimeout(() => {
+ fn.apply(this, args);
+ timer = throttle = false;
+ }, ms);
+ }
+ };
+ }
// Attach click handler to the DT
const annoteDls = window.document.querySelectorAll('dt[data-target-cell]');
for (const annoteDlNode of annoteDls) {
@@ -879,12 +1074,12 @@ S3 access
-
-
@@ -895,7 +1090,9 @@ S3 access
-
+
@@ -904,4 +1101,5 @@ S3 access
+
\ No newline at end of file
diff --git a/docs/Setup-Notes.html b/docs/posts/Setup-Notes.html
similarity index 65%
rename from docs/Setup-Notes.html
rename to docs/posts/Setup-Notes.html
index cbe843e..fa75d56 100644
--- a/docs/Setup-Notes.html
+++ b/docs/posts/Setup-Notes.html
@@ -2,7 +2,7 @@
-
+
@@ -23,31 +23,37 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -74,10 +81,10 @@
-
-
-
-
+
+
+
+
@@ -88,8 +95,8 @@