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.
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+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.
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+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.
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+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;