From adecdfb88c96b230f2c3ed62bc09ccf00828fefd Mon Sep 17 00:00:00 2001 From: Adam Paulen Date: Sun, 19 Jan 2025 03:07:22 +0100 Subject: [PATCH] added pages for other instances --- .gitignore | 2 +- c9088/data/formData.js | 9 + c9088/home.html | 35 + c9088/js/Form.css | 39 ++ c9088/js/FormPage.jsx | 202 ++++++ c9088/js/HomePage.css | 102 +++ c9088/js/HomePage.jsx | 89 +++ c9088/js/Login.css | 38 + c9088/js/LoginPage.jsx | 16 + c9088/js/NotRunning.css | 28 + c9088/js/NotRunning.jsx | 30 + c9088/js/SpawnPending.css | 104 +++ c9088/js/SpawnPending.jsx | 105 +++ c9088/js/form.jsx | 9 + c9088/js/home.jsx | 8 + c9088/js/index.css | 56 ++ c9088/js/login.jsx | 25 + c9088/js/not_running.jsx | 8 + c9088/js/spawn_pending.jsx | 8 + c9088/login.html | 13 + c9088/not_running.html | 21 + c9088/spawn.html | 21 + c9088/spawn_pending.html | 19 + c9088/vite.config.js | 37 + cas/data/formData.js | 10 + cas/home.html | 35 + cas/js/Form.css | 39 ++ cas/js/FormPage.jsx | 202 ++++++ cas/js/HomePage.css | 102 +++ cas/js/HomePage.jsx | 89 +++ cas/js/Login.css | 38 + cas/js/LoginPage.jsx | 16 + cas/js/NotRunning.css | 28 + cas/js/NotRunning.jsx | 30 + cas/js/SpawnPending.css | 104 +++ cas/js/SpawnPending.jsx | 105 +++ cas/js/form.jsx | 9 + cas/js/home.jsx | 8 + cas/js/index.css | 56 ++ cas/js/login.jsx | 25 + cas/js/not_running.jsx | 8 + cas/js/spawn_pending.jsx | 8 + cas/login.html | 13 + cas/not_running.html | 21 + cas/spawn.html | 21 + cas/spawn_pending.html | 19 + cas/vite.config.js | 37 + elter-ri/data/formData.js | 61 ++ elter-ri/home.html | 35 + elter-ri/js/Form.css | 39 ++ elter-ri/js/FormPage.jsx | 662 ++++++++++++++++++ elter-ri/js/HomePage.css | 102 +++ elter-ri/js/HomePage.jsx | 89 +++ elter-ri/js/Login.css | 38 + elter-ri/js/LoginPage.jsx | 16 + elter-ri/js/NotRunning.css | 28 + elter-ri/js/NotRunning.jsx | 30 + elter-ri/js/SpawnPending.css | 104 +++ elter-ri/js/SpawnPending.jsx | 105 +++ elter-ri/js/form.jsx | 9 + elter-ri/js/home.jsx | 8 + elter-ri/js/index.css | 56 ++ elter-ri/js/login.jsx | 25 + elter-ri/js/not_running.jsx | 8 + elter-ri/js/spawn_pending.jsx | 8 + elter-ri/login.html | 13 + elter-ri/not_running.html | 21 + elter-ri/scripts/gatherFormData.js | 110 +++ elter-ri/spawn.html | 21 + elter-ri/spawn_pending.html | 19 + elter-ri/vite.config.js | 37 + src/FormPage.jsx | 2 +- .../DropDownButton/DropDownButton.css | 5 +- src/components/Form/ProgressiveForm.css | 1 + 74 files changed, 3696 insertions(+), 3 deletions(-) create mode 100644 c9088/data/formData.js create mode 100644 c9088/home.html create mode 100644 c9088/js/Form.css create mode 100644 c9088/js/FormPage.jsx create mode 100644 c9088/js/HomePage.css create mode 100644 c9088/js/HomePage.jsx create mode 100644 c9088/js/Login.css create mode 100644 c9088/js/LoginPage.jsx create mode 100644 c9088/js/NotRunning.css create mode 100644 c9088/js/NotRunning.jsx create mode 100644 c9088/js/SpawnPending.css create mode 100644 c9088/js/SpawnPending.jsx create mode 100644 c9088/js/form.jsx create mode 100644 c9088/js/home.jsx create mode 100644 c9088/js/index.css create mode 100644 c9088/js/login.jsx create mode 100644 c9088/js/not_running.jsx create mode 100644 c9088/js/spawn_pending.jsx create mode 100644 c9088/login.html create mode 100644 c9088/not_running.html create mode 100644 c9088/spawn.html create mode 100644 c9088/spawn_pending.html create mode 100644 c9088/vite.config.js create mode 100644 cas/data/formData.js create mode 100644 cas/home.html create mode 100644 cas/js/Form.css create mode 100644 cas/js/FormPage.jsx create mode 100644 cas/js/HomePage.css create mode 100644 cas/js/HomePage.jsx create mode 100644 cas/js/Login.css create mode 100644 cas/js/LoginPage.jsx create mode 100644 cas/js/NotRunning.css create mode 100644 cas/js/NotRunning.jsx create mode 100644 cas/js/SpawnPending.css create mode 100644 cas/js/SpawnPending.jsx create mode 100644 cas/js/form.jsx create mode 100644 cas/js/home.jsx create mode 100644 cas/js/index.css create mode 100644 cas/js/login.jsx create mode 100644 cas/js/not_running.jsx create mode 100644 cas/js/spawn_pending.jsx create mode 100644 cas/login.html create mode 100644 cas/not_running.html create mode 100644 cas/spawn.html create mode 100644 cas/spawn_pending.html create mode 100644 cas/vite.config.js create mode 100644 elter-ri/data/formData.js create mode 100644 elter-ri/home.html create mode 100644 elter-ri/js/Form.css create mode 100644 elter-ri/js/FormPage.jsx create mode 100644 elter-ri/js/HomePage.css create mode 100644 elter-ri/js/HomePage.jsx create mode 100644 elter-ri/js/Login.css create mode 100644 elter-ri/js/LoginPage.jsx create mode 100644 elter-ri/js/NotRunning.css create mode 100644 elter-ri/js/NotRunning.jsx create mode 100644 elter-ri/js/SpawnPending.css create mode 100644 elter-ri/js/SpawnPending.jsx create mode 100644 elter-ri/js/form.jsx create mode 100644 elter-ri/js/home.jsx create mode 100644 elter-ri/js/index.css create mode 100644 elter-ri/js/login.jsx create mode 100644 elter-ri/js/not_running.jsx create mode 100644 elter-ri/js/spawn_pending.jsx create mode 100644 elter-ri/login.html create mode 100644 elter-ri/not_running.html create mode 100644 elter-ri/scripts/gatherFormData.js create mode 100644 elter-ri/spawn.html create mode 100644 elter-ri/spawn_pending.html create mode 100644 elter-ri/vite.config.js diff --git a/.gitignore b/.gitignore index 8668582..ae40786 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ # production /build -dist/ +dist*/ # misc .DS_Store diff --git a/c9088/data/formData.js b/c9088/data/formData.js new file mode 100644 index 0000000..1188420 --- /dev/null +++ b/c9088/data/formData.js @@ -0,0 +1,9 @@ +export const images = { + simple: { + "cerit.io/hubs/rstudio:4.3.1-snakemake": "RStudio with R 4.3.1 and Snakemake", + }, +} + +export const sectionTitles = { + simple: "Simple Jupyter Images", +}; \ No newline at end of file diff --git a/c9088/home.html b/c9088/home.html new file mode 100644 index 0000000..f531736 --- /dev/null +++ b/c9088/home.html @@ -0,0 +1,35 @@ + + + + + + + JupyterHub + + + + {% block main %} {% set named_spawners = + user.all_spawners(include_default=False)|list %} +
+ + + {% endblock main %} + + diff --git a/c9088/js/Form.css b/c9088/js/Form.css new file mode 100644 index 0000000..e6dd03b --- /dev/null +++ b/c9088/js/Form.css @@ -0,0 +1,39 @@ +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; +} + +.GPU-wrapper { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.GPU-stats { + border: none; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/c9088/js/FormPage.jsx b/c9088/js/FormPage.jsx new file mode 100644 index 0000000..9256dc4 --- /dev/null +++ b/c9088/js/FormPage.jsx @@ -0,0 +1,202 @@ +import "./Form.css"; +import React, { useState, useEffect } from "react"; +import ProgressiveForm from "../../src/components/Form/ProgressiveForm"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import { FieldHeader } from "../../src/components/FieldHeader/FieldHeader"; +import { SliderCheckBox } from "../../src/components/SliderCheckBox/SliderCheckBox"; +import { TileSelector } from "../../src/components/TileSelector/TileSelector"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; +import { + DropDownButton, + DropDownOption, +} from "../../src/components/DropDownButton/DropDownButton"; +import { + images, + sectionTitles, +} from "../data/formData"; + +const StepOne = ({ setFormData }) => { + const [activeDropdownIndex, setActiveDropdownIndex] = useState(null); + const [selectedDropdownIndex, setSelectedDropdownIndex] = useState(null); + const [activeDropdownOptionIndex, setActiveDropdownOptionIndex] = useState(null); + + const handleErase = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.delhome = "delete"; + } else { + delete updatedFormData.delhome; + } + + return updatedFormData; + }); + }; + + const handleSelect = (key, image, index, dindex) => { + setSelectedDropdownIndex(dindex); + setActiveDropdownOptionIndex(index); + setFormData((prev) => ({ + ...prev, + dockerimage: image, + })); + }; + + const isActiveIndex = (index) => { + return activeDropdownIndex === index; + }; + + const isSelectedIndex = (index) => { + return selectedDropdownIndex === index; + }; + + let dropDownIndex = 0; + + return ( +
+

Choosing Image

+ {Object.entries(images).map(([key, options], dropdownIndex) => ( + setActiveDropdownIndex(dropdownIndex)} + isSelected={isSelectedIndex(dropdownIndex)} + title={sectionTitles[key]} + > + {Object.entries(options).map(([value, label]) => { + const currentIndex = dropDownIndex++; + return ( + + handleSelect(key, value, currentIndex, dropdownIndex) + } + /> + ); + })} + + ))} +

Choosing storage

+ + + +
+ Mounted to + /home/jovyan +
+
+
+
+ ); +}; + + +const StepThree = ({ setFormData }) => { + + const handleCPUSelect = (value) => { + setFormData((prev) => ({ + ...prev, + cpuselection: value, + })); + }; + + const handleMemSelect = (value) => { + setFormData((prev) => ({ + ...prev, + memselection: value, + })); + }; + + return ( +
+

Resources

+

+ The notebook is spawned only when one node fulfills all your + requirements. +

+ + +
+ ); +}; + +function FormPage() { + + const [formData, setFormData] = useState({ + memselection: 4, + cpuselection: 1, + dockerimage: "cerit.io/hubs/rstudio:4.3.1-snakemake", + }); + + const submitForm = () => { + const formDataToSend = new FormData(); + + Object.entries(formData).forEach(([key, value]) => { + formDataToSend.append(key, value); + }); + + fetch(appConfig.postUrl, { + method: "POST", + body: formDataToSend, + }) + .then((response) => { + if (response.ok) { + const pendingUrl = appConfig.postUrl.replace( + "/spawn/", + "/spawn-pending/", + ); + window.location.href = pendingUrl; + } else { + console.error("Error submitting form:", response.statusText); + } + }) + .catch((error) => { + console.error("Network error:", error); + }); + }; + + const steps = [ + , + , + ]; + + return ( + <> + +
+ + +
+ + ); +} + +export default FormPage; diff --git a/c9088/js/HomePage.css b/c9088/js/HomePage.css new file mode 100644 index 0000000..645c7be --- /dev/null +++ b/c9088/js/HomePage.css @@ -0,0 +1,102 @@ +.default-server-btns { + display: flex; + flex-direction: row; + gap: 0.5rem; + justify-content: center; + width: 100%; +} + +.btn-wrapper { + width: 30%; +} + +.start-named-server { + width: 60%; + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.named-servers { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +input { + width: calc(100%); + box-sizing: border-box; + border-radius: 8px; + background-color: #f2f6f8; + color: black; + border: none; +} + +.action-buttons { + grid-area: btn; + display: grid; + grid-template-columns: 1fr 1fr; + direction: rtl; + grid-auto-flow: dense; + gap: 0.5rem; + font-size: 10px; +} + +.server-properties { + display: grid; + align-items: center; + grid-template-areas: "url url url url url url" + "time time time btn btn btn"; + gap: 0.5rem; +} + +.server-url { + grid-area: url; +} + +.time-col { + grid-area: time; +} + +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + font-size: 12px; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + + .start-named-server { + width: 100%; + display: flex; + flex-direction: row; + gap: 0.5rem; + } + + .btn-wrapper { + width: 45%; + } +} + +p { + font-size: 12px; +} diff --git a/c9088/js/HomePage.jsx b/c9088/js/HomePage.jsx new file mode 100644 index 0000000..a8265df --- /dev/null +++ b/c9088/js/HomePage.jsx @@ -0,0 +1,89 @@ +import "./index.css"; +import "./HomePage.css"; +import { JupyterHubApiClient } from "../../src/api/JupyterHubAPI"; +import React, { useState } from "react"; +import { Button } from "../../src/components/Button/Button"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +function HomePage() { + // for testing with npm run dev please uncomment this block + const appConfig = { + spawners: { + "test": { + last_activity: "2024-11-24T15:48:29.604740Z", + url: "/user/test", + active: true, + ready: false, + }, + "test1": { + last_activity: "2024-11-24T15:46:56.719146Z", + url: "/user/test1", + active: false, + ready: false, + }, + ...Array.from({ length: 0 }, (_, i) => `spawner${i + 1}`).reduce((acc, spawner) => { + acc[spawner] = { + last_activity: new Date().toISOString(), + url: `/user/${spawner}`, + active: Math.random() < 0.5, // Randomly set active status + ready: Math.random() < 0.5, // Randomly set ready status + }; + return acc; + }, {}) + }, + default_server_active: false, + url: "http://localhost", + userName: "dev", + xsrf: "sample-xsrf-token", + }; + + const [defaultServerActive, setDefaultServerActive] = useState( + appConfig.default_server_active, + ); + + const handleStopDefaultServer = async () => { + try { + await apiClient.stopDefaultServer(appConfig.userName); + + setDefaultServerActive(false); + } catch (error) { + console.error(`Failed to stop Default server:`, error.message); + } + }; + + const apiClient = new JupyterHubApiClient("/hub/api", appConfig.xsrf); + + + return ( +
+ +
+
+ {defaultServerActive && ( +
+
+ )} +
+ +
+
+ +
+
+ ); +} + +export default HomePage; diff --git a/c9088/js/Login.css b/c9088/js/Login.css new file mode 100644 index 0000000..b21face --- /dev/null +++ b/c9088/js/Login.css @@ -0,0 +1,38 @@ +.login { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + font-size: 12px; +} + +.wrapper { + display: flex; + flex-direction: column; + position: relative; + justify-content: center; + left: 50%; + transform: translateX(-50%); + width: 50%; + padding-left: 2rem; + padding-right: 2rem; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/c9088/js/LoginPage.jsx b/c9088/js/LoginPage.jsx new file mode 100644 index 0000000..4e64fca --- /dev/null +++ b/c9088/js/LoginPage.jsx @@ -0,0 +1,16 @@ +import "./Login.css"; +import { Button } from "../../src/components/Button/Button"; +import React from "react"; + +function LoginPage({ buttonText, imagePath, link }) { + return ( +
+
+ {imagePath && } + +
+
+ ); +} + +export default LoginPage; diff --git a/c9088/js/NotRunning.css b/c9088/js/NotRunning.css new file mode 100644 index 0000000..fcc0415 --- /dev/null +++ b/c9088/js/NotRunning.css @@ -0,0 +1,28 @@ +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + text-align: center; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/c9088/js/NotRunning.jsx b/c9088/js/NotRunning.jsx new file mode 100644 index 0000000..a2faa79 --- /dev/null +++ b/c9088/js/NotRunning.jsx @@ -0,0 +1,30 @@ +import "./index.css"; +import "./NotRunning.css"; +import React from "react"; +import { Button } from "../../src/components/Button/Button"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +function NotRunning() { + return ( +
+ +
+

Server not running

+ +

Your server is not running. Would you like to start it?

+
+
+ +
+
+ +
+
+ ); +} + +export default NotRunning; diff --git a/c9088/js/SpawnPending.css b/c9088/js/SpawnPending.css new file mode 100644 index 0000000..2db0c95 --- /dev/null +++ b/c9088/js/SpawnPending.css @@ -0,0 +1,104 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100vh; + padding: 16px; + background-color: #f9fbfc; +} + +.text-center { + text-align: center; + margin-bottom: 24px; +} + +#progress-message { + font-size: 16px; + color: #333; + margin-top: 8px; +} + +#progress-details { + width: 100%; + background-color: #f2f6f8; + border-radius: 8px; + padding: 16px; + margin-top: 16px; +} +s #progress-log { + font-size: 14px; + color: #555; + line-height: 1.5; +} + +details summary { + cursor: pointer; + font-weight: bold; + margin-bottom: 8px; + text-align: left; + font-size: 16px; +} + +.progress-log-event { + padding: 8px; + border-bottom: 1px solid #e0e0e0; +} + +.progress-log-event:last-child { + border-bottom: none; +} + +.progress-line { + height: 25px; + background-color: #ccc; + transform: translateY(-50%); + border-radius: 0.5rem; +} + +.progress-line-filled { + height: 25px; + background-color: var(--secondary-color); + transition: width 0.5s ease; + border-radius: 0.5rem; +} + +.progress-line-filled-danger { + height: 25px; + background-color: #dc3545; + transition: width 0.5s ease; + border-radius: 0.5rem; +} + +.message-block { + padding-bottom: 1rem; +} + +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/c9088/js/SpawnPending.jsx b/c9088/js/SpawnPending.jsx new file mode 100644 index 0000000..3795c9b --- /dev/null +++ b/c9088/js/SpawnPending.jsx @@ -0,0 +1,105 @@ +import "./SpawnPending.css"; +import React, { useEffect, useState } from "react"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +const SpawnPending = () => { + const [progress, setProgress] = useState("0"); + + useEffect(() => { + const handleRefresh = () => { + window.location.reload(); + }; + + document + .getElementById("refresh") + ?.addEventListener("click", handleRefresh); + + const evtSource = new EventSource(appConfig.progressUrl); + const progressMessage = document.getElementById("progress-message"); + const progressBar = document.getElementById("progress-line-filled"); + const progressLog = document.getElementById("progress-log"); + + evtSource.onmessage = (e) => { + const evt = JSON.parse(e.data); + console.log(evt); + + if (evt.progress !== undefined) { + setProgress(evt.progress.toString()); + } + + let htmlMessage = ""; + if (evt.html_message !== undefined) { + if (progressMessage) progressMessage.innerHTML = evt.html_message; + htmlMessage = evt.html_message; + } else if (evt.message !== undefined) { + if (progressMessage) progressMessage.textContent = evt.message; + htmlMessage = evt.message; + } + + if (htmlMessage && progressLog) { + const logEvent = document.createElement("div"); + logEvent.className = "progress-log-event"; + logEvent.innerHTML = htmlMessage; + progressLog.appendChild(logEvent); + } + + if (evt.ready) { + evtSource.close(); + window.location.reload(); + } + + if (evt.failed) { + evtSource.close(); + if (progressBar) + progressBar.classList.add("progress-line-filled-danger"); + const progressDetails = document.getElementById("progress-details"); + if (progressDetails) progressDetails.open = true; + } + }; + + return () => { + evtSource.close(); + document + .getElementById("refresh") + ?.removeEventListener("click", handleRefresh); + }; + }, [appConfig.progressUrl]); + + return ( + <> + +
+
+
+
+

Your server is starting up.

+

+ You will be redirected automatically when it's ready for + you. +

+
+
+
+
+
+
+

+
+
+
+ Event log +
+
+
+
+ +
+ + ); +}; + +export default SpawnPending; diff --git a/c9088/js/form.jsx b/c9088/js/form.jsx new file mode 100644 index 0000000..1178522 --- /dev/null +++ b/c9088/js/form.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import FormPage from "./FormPage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +console.log(root); +root.render(); diff --git a/c9088/js/home.jsx b/c9088/js/home.jsx new file mode 100644 index 0000000..d42e8f6 --- /dev/null +++ b/c9088/js/home.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import HomePage from "./HomePage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/c9088/js/index.css b/c9088/js/index.css new file mode 100644 index 0000000..38f7a12 --- /dev/null +++ b/c9088/js/index.css @@ -0,0 +1,56 @@ +body { + background: #e5e7eb; + /*background: radial-gradient(circle at center top, #ffffff, transparent 50%),*/ + /* radial-gradient(circle at center bottom, #ffffff, transparent 50%),*/ + /* radial-gradient(circle at top left, #d8b4ff, transparent 60%),*/ + /* radial-gradient(circle at top right, #b3e0ff, transparent 60%),*/ + /* radial-gradient(circle at bottom left, #e6c8ff, transparent 60%),*/ + /* radial-gradient(circle at bottom right, #80ccff, transparent 60%),*/ + /* linear-gradient(*/ + /* to right,*/ + /* #ffffff 0%,*/ + /* #ffffff 30%,*/ + /* #ffffff 70%,*/ + /* #ffffff 100%*/ + /* ),*/ + /*background-image: url("static/custom-images/bg-transparent.png");*/ + /*background-repeat: no-repeat;*/ + /*background-position: 100% 0;*/ + /*background-size: 70%;*/ + + + font-size: 16px; + position: relative; + + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + font-family: "Montserrat"; + + --primary-color: #2f2557; + --secondary-color: #de73e5; + --hover-color: #4cd9f4; +} + +#root { + width: 100%; + height: 100%; +} + +@media (max-width: 768px) { + #root { + width: 100%; + min-height: 100vh; + } +} + +h2 { + margin: 0.5rem; +} + +p { + margin: 0.5rem; +} diff --git a/c9088/js/login.jsx b/c9088/js/login.jsx new file mode 100644 index 0000000..5656572 --- /dev/null +++ b/c9088/js/login.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import LoginPage from "./LoginPage"; +import AnouncmentMessage from "../../src/components/AnouncmentMessage/AnouncmentMessage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render( +
+ {/**/} + {/*

Scheduled maintenance and reboot on 16th - 18th Dec 2024

*/} + {/*

*/} + {/* {" "}*/} + {/* We will have scheduled maintenance and cluster reboot between 16th and*/} + {/* 17th of December 2024. All running notebooks will be interrupted and*/} + {/* have to be started again.{" "}*/} + {/*

*/} + {/*
*/} + +
, +); diff --git a/c9088/js/not_running.jsx b/c9088/js/not_running.jsx new file mode 100644 index 0000000..3524e71 --- /dev/null +++ b/c9088/js/not_running.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import NotRunning from "./NotRunning"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/c9088/js/spawn_pending.jsx b/c9088/js/spawn_pending.jsx new file mode 100644 index 0000000..2d02d10 --- /dev/null +++ b/c9088/js/spawn_pending.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import SpawnPending from "./SpawnPending"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/c9088/login.html b/c9088/login.html new file mode 100644 index 0000000..88a8db7 --- /dev/null +++ b/c9088/login.html @@ -0,0 +1,13 @@ + + + + + + + JupyterHub + + +
+ + + diff --git a/c9088/not_running.html b/c9088/not_running.html new file mode 100644 index 0000000..b0f23ea --- /dev/null +++ b/c9088/not_running.html @@ -0,0 +1,21 @@ + + + + + + + JupyterHub + + + +
+ + + + diff --git a/c9088/spawn.html b/c9088/spawn.html new file mode 100644 index 0000000..7219ab8 --- /dev/null +++ b/c9088/spawn.html @@ -0,0 +1,21 @@ + + + + + + + JupyterHub + + +
+
{{spawner_options_form | safe}}
+ + + + diff --git a/c9088/spawn_pending.html b/c9088/spawn_pending.html new file mode 100644 index 0000000..4f6a3d8 --- /dev/null +++ b/c9088/spawn_pending.html @@ -0,0 +1,19 @@ + + + + + + + JupyterHub + + +
+ + + + diff --git a/c9088/vite.config.js b/c9088/vite.config.js new file mode 100644 index 0000000..34629e5 --- /dev/null +++ b/c9088/vite.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: __dirname, + base: "/", + build: { + outDir: "../dist_c9088", + rollupOptions: { + input: { + spawn: resolve(__dirname, "spawn.html"), + login: resolve(__dirname, "login.html"), + spawn_pending: resolve(__dirname, "spawn_pending.html"), + home: resolve(__dirname, "home.html"), + not_running: resolve(__dirname, "not_running.html"), + }, + output: { + entryFileNames: "static/custom-js/[name]-[hash].js", + chunkFileNames: "static/custom-js/[name]-[hash].js", + assetFileNames: ({ name }) => { + if (/\.(css)$/.test(name ?? "")) { + return "static/custom-css/[name]-[hash][extname]"; + } else if (/\.(png|jpe?g|gif|svg)$/.test(name ?? "")) { + return "static/custom-css/[name]-[hash][extname]"; + } + return "static/[ext]/[name]-[hash][extname]"; + }, + }, + }, + }, + plugins: [react()], + server: { + open: process.env.ENTRY, + }, +}); diff --git a/cas/data/formData.js b/cas/data/formData.js new file mode 100644 index 0000000..62d7e58 --- /dev/null +++ b/cas/data/formData.js @@ -0,0 +1,10 @@ +export const images = { + simple: { + "cerit.io/hubs/bioconductor:2024-11-21": "Bioconductor Jupyter", + "cerit.io/cerit/rstudio-bioconductor:4.3.1" : "RStudio with R 4.3.1" + }, +} + +export const sectionTitles = { + simple: "Simple Jupyter Images", +}; \ No newline at end of file diff --git a/cas/home.html b/cas/home.html new file mode 100644 index 0000000..f531736 --- /dev/null +++ b/cas/home.html @@ -0,0 +1,35 @@ + + + + + + + JupyterHub + + + + {% block main %} {% set named_spawners = + user.all_spawners(include_default=False)|list %} +
+ + + {% endblock main %} + + diff --git a/cas/js/Form.css b/cas/js/Form.css new file mode 100644 index 0000000..e6dd03b --- /dev/null +++ b/cas/js/Form.css @@ -0,0 +1,39 @@ +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; +} + +.GPU-wrapper { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.GPU-stats { + border: none; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/cas/js/FormPage.jsx b/cas/js/FormPage.jsx new file mode 100644 index 0000000..b1bd59c --- /dev/null +++ b/cas/js/FormPage.jsx @@ -0,0 +1,202 @@ +import "./Form.css"; +import React, { useState, useEffect } from "react"; +import ProgressiveForm from "../../src/components/Form/ProgressiveForm"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import { FieldHeader } from "../../src/components/FieldHeader/FieldHeader"; +import { SliderCheckBox } from "../../src/components/SliderCheckBox/SliderCheckBox"; +import { TileSelector } from "../../src/components/TileSelector/TileSelector"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; +import { + DropDownButton, + DropDownOption, +} from "../../src/components/DropDownButton/DropDownButton"; +import { + images, + sectionTitles, +} from "../data/formData"; + +const StepOne = ({ setFormData }) => { + const [activeDropdownIndex, setActiveDropdownIndex] = useState(null); + const [selectedDropdownIndex, setSelectedDropdownIndex] = useState(null); + const [activeDropdownOptionIndex, setActiveDropdownOptionIndex] = useState(null); + + const handleErase = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.delhome = "delete"; + } else { + delete updatedFormData.delhome; + } + + return updatedFormData; + }); + }; + + const handleSelect = (key, image, index, dindex) => { + setSelectedDropdownIndex(dindex); + setActiveDropdownOptionIndex(index); + setFormData((prev) => ({ + ...prev, + images: image, + })); + }; + + const isActiveIndex = (index) => { + return activeDropdownIndex === index; + }; + + const isSelectedIndex = (index) => { + return selectedDropdownIndex === index; + }; + + let dropDownIndex = 0; + + return ( +
+

Choosing Image

+ {Object.entries(images).map(([key, options], dropdownIndex) => ( + setActiveDropdownIndex(dropdownIndex)} + isSelected={isSelectedIndex(dropdownIndex)} + title={sectionTitles[key]} + > + {Object.entries(options).map(([value, label]) => { + const currentIndex = dropDownIndex++; + return ( + + handleSelect(key, value, currentIndex, dropdownIndex) + } + /> + ); + })} + + ))} +

Choosing storage

+ + + +
+ Mounted to + /home/jovyan +
+
+
+
+ ); +}; + + +const StepThree = ({ setFormData }) => { + + const handleCPUSelect = (value) => { + setFormData((prev) => ({ + ...prev, + cpuselection: value, + })); + }; + + const handleMemSelect = (value) => { + setFormData((prev) => ({ + ...prev, + memselection: value, + })); + }; + + return ( +
+

Resources

+

+ The notebook is spawned only when one node fulfills all your + requirements. +

+ + +
+ ); +}; + +function FormPage() { + + const [formData, setFormData] = useState({ + memselection: 4, + cpuselection: 1, + images: "cerit.io/cerit/rstudio-bioconductor:4.3.1", + }); + + const submitForm = () => { + const formDataToSend = new FormData(); + + Object.entries(formData).forEach(([key, value]) => { + formDataToSend.append(key, value); + }); + + fetch(appConfig.postUrl, { + method: "POST", + body: formDataToSend, + }) + .then((response) => { + if (response.ok) { + const pendingUrl = appConfig.postUrl.replace( + "/spawn/", + "/spawn-pending/", + ); + window.location.href = pendingUrl; + } else { + console.error("Error submitting form:", response.statusText); + } + }) + .catch((error) => { + console.error("Network error:", error); + }); + }; + + const steps = [ + , + , + ]; + + return ( + <> + +
+ + +
+ + ); +} + +export default FormPage; diff --git a/cas/js/HomePage.css b/cas/js/HomePage.css new file mode 100644 index 0000000..645c7be --- /dev/null +++ b/cas/js/HomePage.css @@ -0,0 +1,102 @@ +.default-server-btns { + display: flex; + flex-direction: row; + gap: 0.5rem; + justify-content: center; + width: 100%; +} + +.btn-wrapper { + width: 30%; +} + +.start-named-server { + width: 60%; + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.named-servers { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +input { + width: calc(100%); + box-sizing: border-box; + border-radius: 8px; + background-color: #f2f6f8; + color: black; + border: none; +} + +.action-buttons { + grid-area: btn; + display: grid; + grid-template-columns: 1fr 1fr; + direction: rtl; + grid-auto-flow: dense; + gap: 0.5rem; + font-size: 10px; +} + +.server-properties { + display: grid; + align-items: center; + grid-template-areas: "url url url url url url" + "time time time btn btn btn"; + gap: 0.5rem; +} + +.server-url { + grid-area: url; +} + +.time-col { + grid-area: time; +} + +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + font-size: 12px; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + + .start-named-server { + width: 100%; + display: flex; + flex-direction: row; + gap: 0.5rem; + } + + .btn-wrapper { + width: 45%; + } +} + +p { + font-size: 12px; +} diff --git a/cas/js/HomePage.jsx b/cas/js/HomePage.jsx new file mode 100644 index 0000000..a8265df --- /dev/null +++ b/cas/js/HomePage.jsx @@ -0,0 +1,89 @@ +import "./index.css"; +import "./HomePage.css"; +import { JupyterHubApiClient } from "../../src/api/JupyterHubAPI"; +import React, { useState } from "react"; +import { Button } from "../../src/components/Button/Button"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +function HomePage() { + // for testing with npm run dev please uncomment this block + const appConfig = { + spawners: { + "test": { + last_activity: "2024-11-24T15:48:29.604740Z", + url: "/user/test", + active: true, + ready: false, + }, + "test1": { + last_activity: "2024-11-24T15:46:56.719146Z", + url: "/user/test1", + active: false, + ready: false, + }, + ...Array.from({ length: 0 }, (_, i) => `spawner${i + 1}`).reduce((acc, spawner) => { + acc[spawner] = { + last_activity: new Date().toISOString(), + url: `/user/${spawner}`, + active: Math.random() < 0.5, // Randomly set active status + ready: Math.random() < 0.5, // Randomly set ready status + }; + return acc; + }, {}) + }, + default_server_active: false, + url: "http://localhost", + userName: "dev", + xsrf: "sample-xsrf-token", + }; + + const [defaultServerActive, setDefaultServerActive] = useState( + appConfig.default_server_active, + ); + + const handleStopDefaultServer = async () => { + try { + await apiClient.stopDefaultServer(appConfig.userName); + + setDefaultServerActive(false); + } catch (error) { + console.error(`Failed to stop Default server:`, error.message); + } + }; + + const apiClient = new JupyterHubApiClient("/hub/api", appConfig.xsrf); + + + return ( +
+ +
+
+ {defaultServerActive && ( +
+
+ )} +
+ +
+
+ +
+
+ ); +} + +export default HomePage; diff --git a/cas/js/Login.css b/cas/js/Login.css new file mode 100644 index 0000000..b21face --- /dev/null +++ b/cas/js/Login.css @@ -0,0 +1,38 @@ +.login { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + font-size: 12px; +} + +.wrapper { + display: flex; + flex-direction: column; + position: relative; + justify-content: center; + left: 50%; + transform: translateX(-50%); + width: 50%; + padding-left: 2rem; + padding-right: 2rem; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/cas/js/LoginPage.jsx b/cas/js/LoginPage.jsx new file mode 100644 index 0000000..4e64fca --- /dev/null +++ b/cas/js/LoginPage.jsx @@ -0,0 +1,16 @@ +import "./Login.css"; +import { Button } from "../../src/components/Button/Button"; +import React from "react"; + +function LoginPage({ buttonText, imagePath, link }) { + return ( +
+
+ {imagePath && } + +
+
+ ); +} + +export default LoginPage; diff --git a/cas/js/NotRunning.css b/cas/js/NotRunning.css new file mode 100644 index 0000000..fcc0415 --- /dev/null +++ b/cas/js/NotRunning.css @@ -0,0 +1,28 @@ +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + text-align: center; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/cas/js/NotRunning.jsx b/cas/js/NotRunning.jsx new file mode 100644 index 0000000..a2faa79 --- /dev/null +++ b/cas/js/NotRunning.jsx @@ -0,0 +1,30 @@ +import "./index.css"; +import "./NotRunning.css"; +import React from "react"; +import { Button } from "../../src/components/Button/Button"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +function NotRunning() { + return ( +
+ +
+

Server not running

+ +

Your server is not running. Would you like to start it?

+
+
+ +
+
+ +
+
+ ); +} + +export default NotRunning; diff --git a/cas/js/SpawnPending.css b/cas/js/SpawnPending.css new file mode 100644 index 0000000..2db0c95 --- /dev/null +++ b/cas/js/SpawnPending.css @@ -0,0 +1,104 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100vh; + padding: 16px; + background-color: #f9fbfc; +} + +.text-center { + text-align: center; + margin-bottom: 24px; +} + +#progress-message { + font-size: 16px; + color: #333; + margin-top: 8px; +} + +#progress-details { + width: 100%; + background-color: #f2f6f8; + border-radius: 8px; + padding: 16px; + margin-top: 16px; +} +s #progress-log { + font-size: 14px; + color: #555; + line-height: 1.5; +} + +details summary { + cursor: pointer; + font-weight: bold; + margin-bottom: 8px; + text-align: left; + font-size: 16px; +} + +.progress-log-event { + padding: 8px; + border-bottom: 1px solid #e0e0e0; +} + +.progress-log-event:last-child { + border-bottom: none; +} + +.progress-line { + height: 25px; + background-color: #ccc; + transform: translateY(-50%); + border-radius: 0.5rem; +} + +.progress-line-filled { + height: 25px; + background-color: var(--secondary-color); + transition: width 0.5s ease; + border-radius: 0.5rem; +} + +.progress-line-filled-danger { + height: 25px; + background-color: #dc3545; + transition: width 0.5s ease; + border-radius: 0.5rem; +} + +.message-block { + padding-bottom: 1rem; +} + +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/cas/js/SpawnPending.jsx b/cas/js/SpawnPending.jsx new file mode 100644 index 0000000..3795c9b --- /dev/null +++ b/cas/js/SpawnPending.jsx @@ -0,0 +1,105 @@ +import "./SpawnPending.css"; +import React, { useEffect, useState } from "react"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +const SpawnPending = () => { + const [progress, setProgress] = useState("0"); + + useEffect(() => { + const handleRefresh = () => { + window.location.reload(); + }; + + document + .getElementById("refresh") + ?.addEventListener("click", handleRefresh); + + const evtSource = new EventSource(appConfig.progressUrl); + const progressMessage = document.getElementById("progress-message"); + const progressBar = document.getElementById("progress-line-filled"); + const progressLog = document.getElementById("progress-log"); + + evtSource.onmessage = (e) => { + const evt = JSON.parse(e.data); + console.log(evt); + + if (evt.progress !== undefined) { + setProgress(evt.progress.toString()); + } + + let htmlMessage = ""; + if (evt.html_message !== undefined) { + if (progressMessage) progressMessage.innerHTML = evt.html_message; + htmlMessage = evt.html_message; + } else if (evt.message !== undefined) { + if (progressMessage) progressMessage.textContent = evt.message; + htmlMessage = evt.message; + } + + if (htmlMessage && progressLog) { + const logEvent = document.createElement("div"); + logEvent.className = "progress-log-event"; + logEvent.innerHTML = htmlMessage; + progressLog.appendChild(logEvent); + } + + if (evt.ready) { + evtSource.close(); + window.location.reload(); + } + + if (evt.failed) { + evtSource.close(); + if (progressBar) + progressBar.classList.add("progress-line-filled-danger"); + const progressDetails = document.getElementById("progress-details"); + if (progressDetails) progressDetails.open = true; + } + }; + + return () => { + evtSource.close(); + document + .getElementById("refresh") + ?.removeEventListener("click", handleRefresh); + }; + }, [appConfig.progressUrl]); + + return ( + <> + +
+
+
+
+

Your server is starting up.

+

+ You will be redirected automatically when it's ready for + you. +

+
+
+
+
+
+
+

+
+
+
+ Event log +
+
+
+
+ +
+ + ); +}; + +export default SpawnPending; diff --git a/cas/js/form.jsx b/cas/js/form.jsx new file mode 100644 index 0000000..1178522 --- /dev/null +++ b/cas/js/form.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import FormPage from "./FormPage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +console.log(root); +root.render(); diff --git a/cas/js/home.jsx b/cas/js/home.jsx new file mode 100644 index 0000000..d42e8f6 --- /dev/null +++ b/cas/js/home.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import HomePage from "./HomePage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/cas/js/index.css b/cas/js/index.css new file mode 100644 index 0000000..38f7a12 --- /dev/null +++ b/cas/js/index.css @@ -0,0 +1,56 @@ +body { + background: #e5e7eb; + /*background: radial-gradient(circle at center top, #ffffff, transparent 50%),*/ + /* radial-gradient(circle at center bottom, #ffffff, transparent 50%),*/ + /* radial-gradient(circle at top left, #d8b4ff, transparent 60%),*/ + /* radial-gradient(circle at top right, #b3e0ff, transparent 60%),*/ + /* radial-gradient(circle at bottom left, #e6c8ff, transparent 60%),*/ + /* radial-gradient(circle at bottom right, #80ccff, transparent 60%),*/ + /* linear-gradient(*/ + /* to right,*/ + /* #ffffff 0%,*/ + /* #ffffff 30%,*/ + /* #ffffff 70%,*/ + /* #ffffff 100%*/ + /* ),*/ + /*background-image: url("static/custom-images/bg-transparent.png");*/ + /*background-repeat: no-repeat;*/ + /*background-position: 100% 0;*/ + /*background-size: 70%;*/ + + + font-size: 16px; + position: relative; + + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + font-family: "Montserrat"; + + --primary-color: #2f2557; + --secondary-color: #de73e5; + --hover-color: #4cd9f4; +} + +#root { + width: 100%; + height: 100%; +} + +@media (max-width: 768px) { + #root { + width: 100%; + min-height: 100vh; + } +} + +h2 { + margin: 0.5rem; +} + +p { + margin: 0.5rem; +} diff --git a/cas/js/login.jsx b/cas/js/login.jsx new file mode 100644 index 0000000..5656572 --- /dev/null +++ b/cas/js/login.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import LoginPage from "./LoginPage"; +import AnouncmentMessage from "../../src/components/AnouncmentMessage/AnouncmentMessage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render( +
+ {/**/} + {/*

Scheduled maintenance and reboot on 16th - 18th Dec 2024

*/} + {/*

*/} + {/* {" "}*/} + {/* We will have scheduled maintenance and cluster reboot between 16th and*/} + {/* 17th of December 2024. All running notebooks will be interrupted and*/} + {/* have to be started again.{" "}*/} + {/*

*/} + {/*
*/} + +
, +); diff --git a/cas/js/not_running.jsx b/cas/js/not_running.jsx new file mode 100644 index 0000000..3524e71 --- /dev/null +++ b/cas/js/not_running.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import NotRunning from "./NotRunning"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/cas/js/spawn_pending.jsx b/cas/js/spawn_pending.jsx new file mode 100644 index 0000000..2d02d10 --- /dev/null +++ b/cas/js/spawn_pending.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import SpawnPending from "./SpawnPending"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/cas/login.html b/cas/login.html new file mode 100644 index 0000000..88a8db7 --- /dev/null +++ b/cas/login.html @@ -0,0 +1,13 @@ + + + + + + + JupyterHub + + +
+ + + diff --git a/cas/not_running.html b/cas/not_running.html new file mode 100644 index 0000000..b0f23ea --- /dev/null +++ b/cas/not_running.html @@ -0,0 +1,21 @@ + + + + + + + JupyterHub + + + +
+ + + + diff --git a/cas/spawn.html b/cas/spawn.html new file mode 100644 index 0000000..7219ab8 --- /dev/null +++ b/cas/spawn.html @@ -0,0 +1,21 @@ + + + + + + + JupyterHub + + +
+
{{spawner_options_form | safe}}
+ + + + diff --git a/cas/spawn_pending.html b/cas/spawn_pending.html new file mode 100644 index 0000000..4f6a3d8 --- /dev/null +++ b/cas/spawn_pending.html @@ -0,0 +1,19 @@ + + + + + + + JupyterHub + + +
+ + + + diff --git a/cas/vite.config.js b/cas/vite.config.js new file mode 100644 index 0000000..fe0908d --- /dev/null +++ b/cas/vite.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: __dirname, + base: "/", + build: { + outDir: "../dist_cas", + rollupOptions: { + input: { + spawn: resolve(__dirname, "spawn.html"), + login: resolve(__dirname, "login.html"), + spawn_pending: resolve(__dirname, "spawn_pending.html"), + home: resolve(__dirname, "home.html"), + not_running: resolve(__dirname, "not_running.html"), + }, + output: { + entryFileNames: "static/custom-js/[name]-[hash].js", + chunkFileNames: "static/custom-js/[name]-[hash].js", + assetFileNames: ({ name }) => { + if (/\.(css)$/.test(name ?? "")) { + return "static/custom-css/[name]-[hash][extname]"; + } else if (/\.(png|jpe?g|gif|svg)$/.test(name ?? "")) { + return "static/custom-css/[name]-[hash][extname]"; + } + return "static/[ext]/[name]-[hash][extname]"; + }, + }, + }, + }, + plugins: [react()], + server: { + open: process.env.ENTRY, + }, +}); diff --git a/elter-ri/data/formData.js b/elter-ri/data/formData.js new file mode 100644 index 0000000..550bd6a --- /dev/null +++ b/elter-ri/data/formData.js @@ -0,0 +1,61 @@ +export const images = { + simple: { + "cerit.io/hubs/minimalnb:31-08-2023": "Minimal NB", + "cerit.io/hubs/minimalnb:26-09-2024-ssh": "Minimal NB with SSH access", + "cerit.io/hubs/datasciencenb:31-08-2023": "DataScience NB", + }, + r: { + "cerit.io/hubs/jupyterhubronly:05-02-2024": "Python 3.11 and R 4.3.1 kernels", + "cerit.io/hubs/rstudio:11-08-2022-7": "RStudio with R 4.2.1", + "cerit.io/hubs/rstudio:4.2.1-rsat": "RStudio with R 4.2.1 and RSAT", + "cerit.io/hubs/rstudio:4.3.1": "RStudio with R 4.3.1", + "cerit.io/hubs/rstudio:4.4.0": "RStudio with R 4.4.0", + }, + tf: { + "cerit.io/hubs/tensorflownb:31-08-2023": "TensorFlow 2.10 (CPU only)", + "cerit.io/hubs/tensorflowgpu:2.11.1": "TensorFlow 2.11.1 with GPU and TensorBoard", + "cerit.io/hubs/tensorflowgpu:2.12.1": "TensorFlow 2.12.1 with GPU and TensorBoard", + "cerit.io/hubs/tensorflowgpu:2.15.0": "TensorFlow 2.15.1 with GPU and TensorBoard", + }, + matlab: { + "cerit.io/hubs/matlab:r2022b": "MATLAB R2022b", + "cerit.io/hubs/matlab:r2023a": "MATLAB R2023a", + }, + various: { + "cerit.io/hubs/cuda-ubuntu:12.0-24.04": "CUDA 12.0", + }, +}; + +export const sectionTitles = { + simple: "Simple Jupyter Images", + r: "R Images", + tf: "TensorFlow Images", + matlab: "MATLAB Images", + various: "Various Images", +}; + +export const defaultImagesName = { + simple: "simplenb", + r: "rnb", + tf: "tfnb", + matlab: "matlabnb", + various: "variousnb", +}; + +export const formImagesName = { + simple: "simplenbname", + r: "rnbname", + tf: "tfnbname", + matlab: "matlabnbname", + various: "varnbname", +}; + +export const gpu_instance = { + none: "None", + "mig-1g.10gb": "10GB part A100", + "mig-2g.20gb": "20GB part A100", + a10: "whole A10", + a40: "whole A40", + a100: "whole A100", + any: "any whole gpu", +}; \ No newline at end of file diff --git a/elter-ri/home.html b/elter-ri/home.html new file mode 100644 index 0000000..f531736 --- /dev/null +++ b/elter-ri/home.html @@ -0,0 +1,35 @@ + + + + + + + JupyterHub + + + + {% block main %} {% set named_spawners = + user.all_spawners(include_default=False)|list %} +
+ + + {% endblock main %} + + diff --git a/elter-ri/js/Form.css b/elter-ri/js/Form.css new file mode 100644 index 0000000..e6dd03b --- /dev/null +++ b/elter-ri/js/Form.css @@ -0,0 +1,39 @@ +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; +} + +.GPU-wrapper { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.GPU-stats { + border: none; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/elter-ri/js/FormPage.jsx b/elter-ri/js/FormPage.jsx new file mode 100644 index 0000000..8f3e28a --- /dev/null +++ b/elter-ri/js/FormPage.jsx @@ -0,0 +1,662 @@ +import "./Form.css"; +import React, { useState, useEffect } from "react"; +import ProgressiveForm from "../../src/components/Form/ProgressiveForm"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import { FieldHeader } from "../../src/components/FieldHeader/FieldHeader"; +import { SliderCheckBox } from "../../src/components/SliderCheckBox/SliderCheckBox"; +import { DropDownMenu } from "../../src/components/DropDownMenu/DropDownMenu"; +import { TileSelector } from "../../src/components/TileSelector/TileSelector"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; +import { + DropDownButton, + DropDownOption, +} from "../../src/components/DropDownButton/DropDownButton"; +import { + images, + sectionTitles, + formImagesName, + gpu_instance, + defaultImagesName, +} from "../data/formData"; +import { gatherFormData } from "../scripts/gatherFormData"; + +const StepOne = ({ setFormData, defaultFormData }) => { + const [activeDropdownIndex, setActiveDropdownIndex] = useState(null); + const [checkSsh, setCheckSsh] = useState(null); + const [selectedDropdownIndex, setSelectedDropdownIndex] = useState(null); + const [activeDropdownOptionIndex, setActiveDropdownOptionIndex] = useState(null); + + useEffect(() => { + if (defaultFormData) { + if (defaultFormData.notebookImage) { + const text = defaultFormData.notebookImage.type; + if (text === "customnb") { + handleInputChange({ + target: { + value: defaultFormData.notebookImage.selectedOption, + }, + }, Object.entries(images).length + 1) + } else { + const key = Object.keys(defaultImagesName).find((key) => defaultImagesName[key] === text); + const dindex = Object.keys(images).indexOf(key); + + const flattenedImages = Object.entries(images).flatMap(([category, options]) => + Object.keys(options).map((key) => ({ category, key })) + ); + + const image = defaultFormData.notebookImage.selectedOption.value.replace("cerit.io/hubs/", ""); + const index = flattenedImages.findIndex((entry) => entry.key === image); + + handleSelect(key, image, index, dindex); + } + handleSshCheck(defaultFormData.notebookImage.sshAccess); + setCheckSsh(defaultFormData.notebookImage.sshAccess); + } + + } + }, []); + + const handleSelect = (key, image, index, dindex) => { + setSelectedDropdownIndex(dindex); + setActiveDropdownOptionIndex(index); + setFormData((prev) => ({ + ...prev, + images: key, + [formImagesName[key]]: `cerit.io/hubs/${image}`, + })); + }; + + const handleInputChange = (e, index) => { + setActiveDropdownOptionIndex(null); + setSelectedDropdownIndex(index); + setFormData((prev) => ({ + ...prev, + images: "custom", + customimage: e.target.value, + })); + }; + + const handleSshCheck = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.sshCheck = "yes"; + } else { + delete updatedFormData.sshCheck; + } + + return updatedFormData; + }); + }; + + const isActiveIndex = (index) => { + return activeDropdownIndex === index; + }; + + const isSelectedIndex = (index) => { + return selectedDropdownIndex === index; + }; + + let dropDownIndex = 0; + + function extractName(url) { + const regex = /\/hub\/spawn\/[^/]+\/([^?]*)/; + const match = url.match(regex); + return match ? match[1] : null; + } + + const extractedName = extractName(appConfig.postUrl); + const formattedName = extractedName ? `--${extractedName}` : ""; + + return ( +
+

Choosing Image

+ {Object.entries(images).map(([key, options], dropdownIndex) => ( + setActiveDropdownIndex(dropdownIndex)} + isSelected={isSelectedIndex(dropdownIndex)} + title={sectionTitles[key]} + > + {Object.entries(options).map(([value, label]) => { + const currentIndex = dropDownIndex++; + return ( + + handleSelect(key, value, currentIndex, dropdownIndex) + } + /> + ); + })} + + ))} + + setActiveDropdownIndex(Object.entries(images).length + 1) + } + title="Custom Image" + infoText="Provide image name in format repo/image_name:tag" + > + + handleInputChange(e, Object.entries(images).length + 1) + } + placeholder="Write image name here" + className="custom-option" + /> + + + Connection will be available at jovyan@jupyter-{appConfig.userName}{formattedName}.dyn.cloud.e-infra.cz + +
+ ); +}; + +const StepTwo = ({ setFormData, formData, defaultFormData }) => { + const [activeDropdownIndex, setActiveDropdownIndex] = useState(null); + const [defaultOptionPhname, setDefaultOptionPhname] = useState(null); + const [checkedErased, setCheckErased] = useState(false); + const [checkedDirectories, setCheckedDirectories] = useState(false); + const [checkedStorage, setCheckedStorage] = useState(false); + const [defaultHome, setDefaultHome] = useState(false); + const [checkedMount, setCheckedMount] = useState(false); + + useEffect(() => { + if (defaultFormData) { + if (defaultFormData.persistentHome) { + + + const text = defaultFormData.persistentHome.type; + setFormData((prev) => ({ + ...prev, + phselection: text, + })); + setActiveDropdownIndex(text === "new" ? 0 : 1); + const check = defaultFormData.persistentHome.eraseIfExists + if (check) { + setFormData((prev) => ({ + ...prev, + phCheck: check, + })); + setCheckErased(check) + } + const phname = defaultFormData.persistentHome.selectedHome + if (phname) { + const name = defaultFormData.persistentHome.selectedHome.value; + setFormData((prev) => ({ + ...prev, + phname: name, + })); + setDefaultOptionPhname([name, name]); + } + + const projectDirectories = defaultFormData.projectDirectories + + if (projectDirectories) { + setFormData((prev) => ({ + ...prev, + projectCheck: "yes" + })); + + setCheckedDirectories(projectDirectories) + } + + const metaCentrumHome = defaultFormData.metaCentrumHome + + if (metaCentrumHome) { + + const enabled = metaCentrumHome.enabled + + if (enabled) { + setFormData((prev) => ({ + ...prev, + storageCheck: "yes" + })); + + setCheckedStorage(enabled) + + const selectedHome = metaCentrumHome.selectedHome.value; + + setFormData((prev) => ({ + ...prev, + home: selectedHome + })); + + setDefaultHome([selectedHome, selectedHome]) + + const mountToStorage = metaCentrumHome.mountToStorage; + + if(mountToStorage) { + setFormData((prev) => ({ + ...prev, + locationStorageCheck: "yes" + })); + + setCheckedMount(mountToStorage) + } + } + } + } + + } + }, []); + + const handleStorage = (storage) => { + setFormData((prev) => ({ + ...prev, + home: storage, + })); + }; + + const handlePersistentHome = (val) => { + setFormData((prev) => ({ + ...prev, + phname: val, + })); + }; + + const handleStorageCheck = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.storageCheck = "yes"; + } else { + delete updatedFormData.storageCheck; + } + + return updatedFormData; + }); + }; + + const handleCheckboxDirectories = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.projectCheck = "yes"; + } else { + delete updatedFormData.projectCheck; + } + + return updatedFormData; + }); + }; + + const handleErase = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.phCheck = "yes"; + } else { + delete updatedFormData.phCheck; + } + + return updatedFormData; + }); + }; + + const handlePersistentNewSelect = (index) => { + setActiveDropdownIndex(index); + setFormData((prev) => ({ + ...prev, + phselection: "new", + })); + }; + + const handleExisting = (index) => { + setActiveDropdownIndex(index); + setFormData((prev) => ({ + ...prev, + phselection: false, + })); + }; + + const handleLocationStorageCheck = (checked) => { + setFormData((prev) => { + const updatedFormData = { ...prev }; + + if (checked) { + updatedFormData.locationStorageCheck = "yes"; + } else { + delete updatedFormData.locationStorageCheck; + } + + return updatedFormData; + }); + }; + + const values = {}; + const selectElement = document.getElementById("phid"); + + if (selectElement !== null) { + const options = selectElement.getElementsByTagName("option"); + for (let option of options) { + values[option.value] = option.value; + } + } else { + values["testing"] = "testing"; + } + + return ( +
+

Choosing storage

+ + handlePersistentNewSelect(0)} + primary={false} + title="New" + > + +
+ Mounted to + /home/jovyan +
+
+
+
+ ); +}; + +const StepThree = ({ setFormData, defaultFormData }) => { + const [defMem, setDefMem] = useState(null); + const [defCPU, setDefCPU] = useState(null); + const [defGPU, setDefGPU] = useState(null); + + useEffect(() => { + if (defaultFormData) { + const mem = Number(defaultFormData.memory.value); + const cpu = defaultFormData.cpu; + const val = defaultFormData.gpu.value; + const txt = defaultFormData.gpu.text; + + setFormData((prev) => ({ + ...prev, + cpuselection: cpu, + })); + + setFormData((prev) => ({ + ...prev, + memselection: mem, + })); + + setFormData((prev) => ({ + ...prev, + gpuselection: val, + })); + + setDefMem(mem) + setDefCPU(cpu) + setDefGPU([val, txt]) + } + }, []); + + const handleCPUSelect = (value) => { + setFormData((prev) => ({ + ...prev, + cpuselection: value, + })); + }; + + const handleMemSelect = (value) => { + setFormData((prev) => ({ + ...prev, + memselection: value, + })); + }; + + const handleGPUSelect = (value) => { + setFormData((prev) => ({ + ...prev, + gpuselection: value, + })); + }; + + return ( +
+ {defCPU !== null && defMem !== null && defGPU !== null ? ( <> +

Resources

+

+ The notebook is spawned only when one node fulfills all your + requirements. +

+ + + +

By default, no GPU is assigned.

+ +

Current GPUs Free:

+
+
+ + + + + + +
+
+
+ +
+ + +
+
+ ) : <>} +
+ ); +}; + +function FormPage() { + + var defaultFormData = gatherFormData(); + console.log(defaultFormData); + if (defaultFormData === null) { + defaultFormData = { + "memory": { + "value": "256", + "text": "256" + }, + "gpu": { + "value": "a10", + "text": "whole A10" + }, + "cpu": 1, + "metaCentrumHome": { + "enabled": true, + "selectedHome": { + "value": "du-cesnet", + "text": "du-cesnet" + }, + "mountToStorage": false + }, + "projectDirectories": true, + "persistentHome": { + "type": "new", + "eraseIfExists": false + }, + "notebookImage": { + "type": "variousnb", + "selectedOption": { + "value": "cerit.io/hubs/colab:2024-10-17", + "text": "Google Colab" + }, + "sshAccess": true + } + } + } + + const [error, setError] = useState(""); + const [formData, setFormData] = useState({ + memselection: 4, + cpuselection: 1, + gpuselection: "none", + migamount: 1, + }); + + const submitForm = () => { + const requiredFields = { + images: "Image", + phselection: "Persistent Notebook Home", + }; + + const missingFields = Object.keys(requiredFields) + .filter((key) => formData[key] === undefined) + .map((key) => requiredFields[key]); + + if (missingFields.length > 0) { + setError(`Please select the following: ${missingFields.join(", ")}`); + return; + } + + setError(""); + + const formDataToSend = new FormData(); + + Object.entries(formData).forEach(([key, value]) => { + formDataToSend.append(key, value); + }); + + fetch(appConfig.postUrl, { + method: "POST", + body: formDataToSend, + }) + .then((response) => { + if (response.ok) { + const pendingUrl = appConfig.postUrl.replace( + "/spawn/", + "/spawn-pending/", + ); + window.location.href = pendingUrl; + } else { + console.error("Error submitting form:", response.statusText); + } + }) + .catch((error) => { + console.error("Network error:", error); + }); + }; + + const steps = [ + , + , + , + ]; + + return ( + <> + +
+ + +
+ + ); +} + +export default FormPage; diff --git a/elter-ri/js/HomePage.css b/elter-ri/js/HomePage.css new file mode 100644 index 0000000..645c7be --- /dev/null +++ b/elter-ri/js/HomePage.css @@ -0,0 +1,102 @@ +.default-server-btns { + display: flex; + flex-direction: row; + gap: 0.5rem; + justify-content: center; + width: 100%; +} + +.btn-wrapper { + width: 30%; +} + +.start-named-server { + width: 60%; + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.named-servers { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +input { + width: calc(100%); + box-sizing: border-box; + border-radius: 8px; + background-color: #f2f6f8; + color: black; + border: none; +} + +.action-buttons { + grid-area: btn; + display: grid; + grid-template-columns: 1fr 1fr; + direction: rtl; + grid-auto-flow: dense; + gap: 0.5rem; + font-size: 10px; +} + +.server-properties { + display: grid; + align-items: center; + grid-template-areas: "url url url url url url" + "time time time btn btn btn"; + gap: 0.5rem; +} + +.server-url { + grid-area: url; +} + +.time-col { + grid-area: time; +} + +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + font-size: 12px; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + + .start-named-server { + width: 100%; + display: flex; + flex-direction: row; + gap: 0.5rem; + } + + .btn-wrapper { + width: 45%; + } +} + +p { + font-size: 12px; +} diff --git a/elter-ri/js/HomePage.jsx b/elter-ri/js/HomePage.jsx new file mode 100644 index 0000000..a8265df --- /dev/null +++ b/elter-ri/js/HomePage.jsx @@ -0,0 +1,89 @@ +import "./index.css"; +import "./HomePage.css"; +import { JupyterHubApiClient } from "../../src/api/JupyterHubAPI"; +import React, { useState } from "react"; +import { Button } from "../../src/components/Button/Button"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +function HomePage() { + // for testing with npm run dev please uncomment this block + const appConfig = { + spawners: { + "test": { + last_activity: "2024-11-24T15:48:29.604740Z", + url: "/user/test", + active: true, + ready: false, + }, + "test1": { + last_activity: "2024-11-24T15:46:56.719146Z", + url: "/user/test1", + active: false, + ready: false, + }, + ...Array.from({ length: 0 }, (_, i) => `spawner${i + 1}`).reduce((acc, spawner) => { + acc[spawner] = { + last_activity: new Date().toISOString(), + url: `/user/${spawner}`, + active: Math.random() < 0.5, // Randomly set active status + ready: Math.random() < 0.5, // Randomly set ready status + }; + return acc; + }, {}) + }, + default_server_active: false, + url: "http://localhost", + userName: "dev", + xsrf: "sample-xsrf-token", + }; + + const [defaultServerActive, setDefaultServerActive] = useState( + appConfig.default_server_active, + ); + + const handleStopDefaultServer = async () => { + try { + await apiClient.stopDefaultServer(appConfig.userName); + + setDefaultServerActive(false); + } catch (error) { + console.error(`Failed to stop Default server:`, error.message); + } + }; + + const apiClient = new JupyterHubApiClient("/hub/api", appConfig.xsrf); + + + return ( +
+ +
+
+ {defaultServerActive && ( +
+
+ )} +
+ +
+
+ +
+
+ ); +} + +export default HomePage; diff --git a/elter-ri/js/Login.css b/elter-ri/js/Login.css new file mode 100644 index 0000000..b21face --- /dev/null +++ b/elter-ri/js/Login.css @@ -0,0 +1,38 @@ +.login { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + font-size: 12px; +} + +.wrapper { + display: flex; + flex-direction: column; + position: relative; + justify-content: center; + left: 50%; + transform: translateX(-50%); + width: 50%; + padding-left: 2rem; + padding-right: 2rem; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 100vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/elter-ri/js/LoginPage.jsx b/elter-ri/js/LoginPage.jsx new file mode 100644 index 0000000..4e64fca --- /dev/null +++ b/elter-ri/js/LoginPage.jsx @@ -0,0 +1,16 @@ +import "./Login.css"; +import { Button } from "../../src/components/Button/Button"; +import React from "react"; + +function LoginPage({ buttonText, imagePath, link }) { + return ( +
+
+ {imagePath && } + +
+
+ ); +} + +export default LoginPage; diff --git a/elter-ri/js/NotRunning.css b/elter-ri/js/NotRunning.css new file mode 100644 index 0000000..fcc0415 --- /dev/null +++ b/elter-ri/js/NotRunning.css @@ -0,0 +1,28 @@ +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + text-align: center; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/elter-ri/js/NotRunning.jsx b/elter-ri/js/NotRunning.jsx new file mode 100644 index 0000000..a2faa79 --- /dev/null +++ b/elter-ri/js/NotRunning.jsx @@ -0,0 +1,30 @@ +import "./index.css"; +import "./NotRunning.css"; +import React from "react"; +import { Button } from "../../src/components/Button/Button"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +function NotRunning() { + return ( +
+ +
+

Server not running

+ +

Your server is not running. Would you like to start it?

+
+
+ +
+
+ +
+
+ ); +} + +export default NotRunning; diff --git a/elter-ri/js/SpawnPending.css b/elter-ri/js/SpawnPending.css new file mode 100644 index 0000000..2db0c95 --- /dev/null +++ b/elter-ri/js/SpawnPending.css @@ -0,0 +1,104 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100vh; + padding: 16px; + background-color: #f9fbfc; +} + +.text-center { + text-align: center; + margin-bottom: 24px; +} + +#progress-message { + font-size: 16px; + color: #333; + margin-top: 8px; +} + +#progress-details { + width: 100%; + background-color: #f2f6f8; + border-radius: 8px; + padding: 16px; + margin-top: 16px; +} +s #progress-log { + font-size: 14px; + color: #555; + line-height: 1.5; +} + +details summary { + cursor: pointer; + font-weight: bold; + margin-bottom: 8px; + text-align: left; + font-size: 16px; +} + +.progress-log-event { + padding: 8px; + border-bottom: 1px solid #e0e0e0; +} + +.progress-log-event:last-child { + border-bottom: none; +} + +.progress-line { + height: 25px; + background-color: #ccc; + transform: translateY(-50%); + border-radius: 0.5rem; +} + +.progress-line-filled { + height: 25px; + background-color: var(--secondary-color); + transition: width 0.5s ease; + border-radius: 0.5rem; +} + +.progress-line-filled-danger { + height: 25px; + background-color: #dc3545; + transition: width 0.5s ease; + border-radius: 0.5rem; +} + +.message-block { + padding-bottom: 1rem; +} + +.wrapper { + display: flex; + flex-direction: column; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 50%; + background: rgba(255, 255, 255, 0.5); + padding: 50px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; +} + +@media (max-width: 768px) { + .wrapper { + display: flex; + flex-direction: column; + position: absolute; + width: 90%; + padding: 5%; + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + min-height: 80vh; + } +} diff --git a/elter-ri/js/SpawnPending.jsx b/elter-ri/js/SpawnPending.jsx new file mode 100644 index 0000000..3795c9b --- /dev/null +++ b/elter-ri/js/SpawnPending.jsx @@ -0,0 +1,105 @@ +import "./SpawnPending.css"; +import React, { useEffect, useState } from "react"; +import { EinfraFooter } from "../../src/components/FooterAndHeader/EinfraFooter"; +import JupyterHubHeader from "../../src/components/FooterAndHeader/JupyterHubHeader"; + +const SpawnPending = () => { + const [progress, setProgress] = useState("0"); + + useEffect(() => { + const handleRefresh = () => { + window.location.reload(); + }; + + document + .getElementById("refresh") + ?.addEventListener("click", handleRefresh); + + const evtSource = new EventSource(appConfig.progressUrl); + const progressMessage = document.getElementById("progress-message"); + const progressBar = document.getElementById("progress-line-filled"); + const progressLog = document.getElementById("progress-log"); + + evtSource.onmessage = (e) => { + const evt = JSON.parse(e.data); + console.log(evt); + + if (evt.progress !== undefined) { + setProgress(evt.progress.toString()); + } + + let htmlMessage = ""; + if (evt.html_message !== undefined) { + if (progressMessage) progressMessage.innerHTML = evt.html_message; + htmlMessage = evt.html_message; + } else if (evt.message !== undefined) { + if (progressMessage) progressMessage.textContent = evt.message; + htmlMessage = evt.message; + } + + if (htmlMessage && progressLog) { + const logEvent = document.createElement("div"); + logEvent.className = "progress-log-event"; + logEvent.innerHTML = htmlMessage; + progressLog.appendChild(logEvent); + } + + if (evt.ready) { + evtSource.close(); + window.location.reload(); + } + + if (evt.failed) { + evtSource.close(); + if (progressBar) + progressBar.classList.add("progress-line-filled-danger"); + const progressDetails = document.getElementById("progress-details"); + if (progressDetails) progressDetails.open = true; + } + }; + + return () => { + evtSource.close(); + document + .getElementById("refresh") + ?.removeEventListener("click", handleRefresh); + }; + }, [appConfig.progressUrl]); + + return ( + <> + +
+
+
+
+

Your server is starting up.

+

+ You will be redirected automatically when it's ready for + you. +

+
+
+
+
+
+
+

+
+
+
+ Event log +
+
+
+
+ +
+ + ); +}; + +export default SpawnPending; diff --git a/elter-ri/js/form.jsx b/elter-ri/js/form.jsx new file mode 100644 index 0000000..1178522 --- /dev/null +++ b/elter-ri/js/form.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import FormPage from "./FormPage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +console.log(root); +root.render(); diff --git a/elter-ri/js/home.jsx b/elter-ri/js/home.jsx new file mode 100644 index 0000000..d42e8f6 --- /dev/null +++ b/elter-ri/js/home.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import HomePage from "./HomePage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/elter-ri/js/index.css b/elter-ri/js/index.css new file mode 100644 index 0000000..38f7a12 --- /dev/null +++ b/elter-ri/js/index.css @@ -0,0 +1,56 @@ +body { + background: #e5e7eb; + /*background: radial-gradient(circle at center top, #ffffff, transparent 50%),*/ + /* radial-gradient(circle at center bottom, #ffffff, transparent 50%),*/ + /* radial-gradient(circle at top left, #d8b4ff, transparent 60%),*/ + /* radial-gradient(circle at top right, #b3e0ff, transparent 60%),*/ + /* radial-gradient(circle at bottom left, #e6c8ff, transparent 60%),*/ + /* radial-gradient(circle at bottom right, #80ccff, transparent 60%),*/ + /* linear-gradient(*/ + /* to right,*/ + /* #ffffff 0%,*/ + /* #ffffff 30%,*/ + /* #ffffff 70%,*/ + /* #ffffff 100%*/ + /* ),*/ + /*background-image: url("static/custom-images/bg-transparent.png");*/ + /*background-repeat: no-repeat;*/ + /*background-position: 100% 0;*/ + /*background-size: 70%;*/ + + + font-size: 16px; + position: relative; + + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + font-family: "Montserrat"; + + --primary-color: #2f2557; + --secondary-color: #de73e5; + --hover-color: #4cd9f4; +} + +#root { + width: 100%; + height: 100%; +} + +@media (max-width: 768px) { + #root { + width: 100%; + min-height: 100vh; + } +} + +h2 { + margin: 0.5rem; +} + +p { + margin: 0.5rem; +} diff --git a/elter-ri/js/login.jsx b/elter-ri/js/login.jsx new file mode 100644 index 0000000..5656572 --- /dev/null +++ b/elter-ri/js/login.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import LoginPage from "./LoginPage"; +import AnouncmentMessage from "../../src/components/AnouncmentMessage/AnouncmentMessage"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render( +
+ {/**/} + {/*

Scheduled maintenance and reboot on 16th - 18th Dec 2024

*/} + {/*

*/} + {/* {" "}*/} + {/* We will have scheduled maintenance and cluster reboot between 16th and*/} + {/* 17th of December 2024. All running notebooks will be interrupted and*/} + {/* have to be started again.{" "}*/} + {/*

*/} + {/*
*/} + +
, +); diff --git a/elter-ri/js/not_running.jsx b/elter-ri/js/not_running.jsx new file mode 100644 index 0000000..3524e71 --- /dev/null +++ b/elter-ri/js/not_running.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import NotRunning from "./NotRunning"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/elter-ri/js/spawn_pending.jsx b/elter-ri/js/spawn_pending.jsx new file mode 100644 index 0000000..2d02d10 --- /dev/null +++ b/elter-ri/js/spawn_pending.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import SpawnPending from "./SpawnPending"; +import { createRoot } from "react-dom/client"; +import "@fontsource/montserrat/600.css"; +import "./index.css"; + +const root = createRoot(document.getElementById("root")); +root.render(); diff --git a/elter-ri/login.html b/elter-ri/login.html new file mode 100644 index 0000000..88a8db7 --- /dev/null +++ b/elter-ri/login.html @@ -0,0 +1,13 @@ + + + + + + + JupyterHub + + +
+ + + diff --git a/elter-ri/not_running.html b/elter-ri/not_running.html new file mode 100644 index 0000000..b0f23ea --- /dev/null +++ b/elter-ri/not_running.html @@ -0,0 +1,21 @@ + + + + + + + JupyterHub + + + +
+ + + + diff --git a/elter-ri/scripts/gatherFormData.js b/elter-ri/scripts/gatherFormData.js new file mode 100644 index 0000000..5fd98a8 --- /dev/null +++ b/elter-ri/scripts/gatherFormData.js @@ -0,0 +1,110 @@ +function getMemorySelection() { + const memSelect = document.getElementById('memselection'); + if (memSelect) { + return { + value: memSelect.value, + text: memSelect.options[memSelect.selectedIndex].text + }; + } + return null; +} + +function getGpuSelection() { + const gpuSelect = document.getElementById('gpuid'); + if (gpuSelect) { + const gpuData = { + value: gpuSelect.value, + text: gpuSelect.options[gpuSelect.selectedIndex].text + }; + + if (gpuSelect.value.startsWith('mig')) { + const migAmountSelect = document.getElementById('migamountselection'); + gpuData.migAmount = migAmountSelect + ? { + value: migAmountSelect.value, + text: migAmountSelect.options[migAmountSelect.selectedIndex].text + } + : null; + } + return gpuData; + } + return null; +} + +function getCpuSelection() { + const cpuInput = document.getElementById('cpuinput'); + if (cpuInput) { + const cpuValue = parseInt(cpuInput.value, 10); + return cpuValue >= 1 && cpuValue <= 32 ? cpuValue : 'Out of range'; + } + return null; +} + +function getPersistentHomeSelection() { + const delhome = document.getElementById('delhome'); + const delhomeData = delhome && delhome.checked + + return delhomeData; +} + +function getNotebookImageSelection() { + const checkedRadio = document.querySelector('input[name="images"]:checked'); + const notebookImageData = { type: null, selectedOption: null, sshAccess: null }; + + if (checkedRadio) { + notebookImageData.type = checkedRadio.id; + const selectMap = { + simplenb: 'simplenblistselection', + rnb: 'rnblistselection', + tfnb: 'tfnblistselection', + matlabnb: 'matlabnblistselection', + variousnb: 'variousnblistselection', + customnb: 'customimage' + }; + const selectId = selectMap[checkedRadio.id]; + if (selectId) { + if (selectId === 'customimage') { + const customInput = document.getElementById(selectId); + if (customInput) { + notebookImageData.selectedOption = customInput.value; + } + } else { + const selectElement = document.getElementById(selectId); + if (selectElement) { + notebookImageData.selectedOption = { + value: selectElement.value, + text: selectElement.options[selectElement.selectedIndex].text + }; + } + } + } + } + const sshCheckbox = document.getElementById('sshaccess'); + notebookImageData.sshAccess = sshCheckbox && sshCheckbox.checked; + return notebookImageData; +} + +function isEmpty(value) { + if (value == null) { + return true; + } + if (typeof value === "object" && !Array.isArray(value)) { + return Object.values(value).every(isEmpty); + } + return false; + } + +export function gatherFormData() { + const formData = { + memory: getMemorySelection(), + gpu: getGpuSelection(), + cpu: getCpuSelection(), + delhome: getPersistentHomeSelection(), + notebookImage: getNotebookImageSelection() + }; + + const allEmpty = Object.values(formData).every(isEmpty); + + return allEmpty ? null : formData; +} + diff --git a/elter-ri/spawn.html b/elter-ri/spawn.html new file mode 100644 index 0000000..7219ab8 --- /dev/null +++ b/elter-ri/spawn.html @@ -0,0 +1,21 @@ + + + + + + + JupyterHub + + +
+
{{spawner_options_form | safe}}
+ + + + diff --git a/elter-ri/spawn_pending.html b/elter-ri/spawn_pending.html new file mode 100644 index 0000000..4f6a3d8 --- /dev/null +++ b/elter-ri/spawn_pending.html @@ -0,0 +1,19 @@ + + + + + + + JupyterHub + + +
+ + + + diff --git a/elter-ri/vite.config.js b/elter-ri/vite.config.js new file mode 100644 index 0000000..fe0908d --- /dev/null +++ b/elter-ri/vite.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + root: __dirname, + base: "/", + build: { + outDir: "../dist_cas", + rollupOptions: { + input: { + spawn: resolve(__dirname, "spawn.html"), + login: resolve(__dirname, "login.html"), + spawn_pending: resolve(__dirname, "spawn_pending.html"), + home: resolve(__dirname, "home.html"), + not_running: resolve(__dirname, "not_running.html"), + }, + output: { + entryFileNames: "static/custom-js/[name]-[hash].js", + chunkFileNames: "static/custom-js/[name]-[hash].js", + assetFileNames: ({ name }) => { + if (/\.(css)$/.test(name ?? "")) { + return "static/custom-css/[name]-[hash][extname]"; + } else if (/\.(png|jpe?g|gif|svg)$/.test(name ?? "")) { + return "static/custom-css/[name]-[hash][extname]"; + } + return "static/[ext]/[name]-[hash][extname]"; + }, + }, + }, + }, + plugins: [react()], + server: { + open: process.env.ENTRY, + }, +}); diff --git a/src/FormPage.jsx b/src/FormPage.jsx index 32323ab..ea4e883 100644 --- a/src/FormPage.jsx +++ b/src/FormPage.jsx @@ -599,7 +599,7 @@ const StepThree = ({ setFormData, defaultFormData }) => { > - ) : <>}; + ) : <>} ); }; diff --git a/src/components/DropDownButton/DropDownButton.css b/src/components/DropDownButton/DropDownButton.css index 240c1b7..11a53f9 100644 --- a/src/components/DropDownButton/DropDownButton.css +++ b/src/components/DropDownButton/DropDownButton.css @@ -3,7 +3,6 @@ display: flex; flex-direction: column; width: 100%; - padding: 0.6rem; } .activeText { @@ -46,6 +45,8 @@ grid-template-areas: "title . text . icon"; grid-template-columns: auto 25fr auto 1fr auto; + font-family: "Montserrat"; + } .dropbtn--primary.selected { @@ -146,6 +147,8 @@ cursor: pointer; margin-bottom: 4px; justify-content: space-between; + + font-family: "Montserrat"; } .dropbtn--secondary.active { diff --git a/src/components/Form/ProgressiveForm.css b/src/components/Form/ProgressiveForm.css index 4b471ac..d4c1490 100644 --- a/src/components/Form/ProgressiveForm.css +++ b/src/components/Form/ProgressiveForm.css @@ -11,6 +11,7 @@ } .btns_wrap { + padding: 0.5rem; display: flex; justify-content: flex-end; gap: 5rem;