From 375ab0f6a81787eaea6c4849fd90533844e7e79d Mon Sep 17 00:00:00 2001 From: Chad Roberts Date: Mon, 29 Nov 2021 13:21:40 -0500 Subject: [PATCH] Revert "[UPSTREAM] Add spawn progress watch modal, replace spawn_pending page (#47)" This reverts commit 0ce24829d83f3a988b84ad26de9594afa4c27c0b. --- jupyterhub_singleuser_profiles/ui/.gitignore | 2 +- .../ui/package.json | 2 +- .../ui/src/App/App.scss | 11 - .../ui/src/App/Spawner.tsx | 62 ++-- .../ui/src/App/StartServerModal.scss | 88 ----- .../ui/src/App/StartServerModal.tsx | 303 ------------------ .../ui/src/ImageForm/ImageForm.tsx | 5 +- .../ui/src/__mock__/mockData.ts | 108 ------- .../ui/src/index.html | 24 +- .../ui/src/utils/HubCalls.ts | 18 +- .../ui/src/utils/useWatchSpawnProgress.tsx | 56 ---- .../ui/templates/spawn.html | 29 ++ 12 files changed, 66 insertions(+), 642 deletions(-) delete mode 100644 jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.scss delete mode 100644 jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.tsx delete mode 100644 jupyterhub_singleuser_profiles/ui/src/utils/useWatchSpawnProgress.tsx create mode 100644 jupyterhub_singleuser_profiles/ui/templates/spawn.html diff --git a/jupyterhub_singleuser_profiles/ui/.gitignore b/jupyterhub_singleuser_profiles/ui/.gitignore index 16db23c0..dcd5bd33 100644 --- a/jupyterhub_singleuser_profiles/ui/.gitignore +++ b/jupyterhub_singleuser_profiles/ui/.gitignore @@ -5,7 +5,7 @@ /build # Created templates -/templates +/templates/admin.html *stats.json diff --git a/jupyterhub_singleuser_profiles/ui/package.json b/jupyterhub_singleuser_profiles/ui/package.json index 733f3473..6445ccb7 100644 --- a/jupyterhub_singleuser_profiles/ui/package.json +++ b/jupyterhub_singleuser_profiles/ui/package.json @@ -18,7 +18,7 @@ }, "scripts": { "build": "run-s build:prod", - "postbuild": "mkdir -p templates; cp build/index.html templates/spawn.html; cp build/index.html templates/spawn_pending.html; cp build/admin.html templates/admin.html", + "postbuild": "cp build/admin.html templates/admin.html", "build:analyze": "export _JSP_OUTPUT_ONLY=true; run-s build build:bundle-profile build:bundle-analyze", "build:bundle-profile": "webpack --config ./config/webpack.prod.js --json ./bundle.stats.json", "build:bundle-analyze": "webpack-bundle-analyzer ./bundle.stats.json", diff --git a/jupyterhub_singleuser_profiles/ui/src/App/App.scss b/jupyterhub_singleuser_profiles/ui/src/App/App.scss index 585740d6..d8333de1 100644 --- a/jupyterhub_singleuser_profiles/ui/src/App/App.scss +++ b/jupyterhub_singleuser_profiles/ui/src/App/App.scss @@ -1,15 +1,4 @@ -html { - height: 100%; -} - -body, #root { - display: flex; - flex: 1; - flex-direction: column; -} - .jsp-app { - flex: 1; position: relative; padding: 20px var(--pf-global--spacer--2xl) !important; margin-top: -20px !important; diff --git a/jupyterhub_singleuser_profiles/ui/src/App/Spawner.tsx b/jupyterhub_singleuser_profiles/ui/src/App/Spawner.tsx index c2f0571c..8c6fe8f1 100644 --- a/jupyterhub_singleuser_profiles/ui/src/App/Spawner.tsx +++ b/jupyterhub_singleuser_profiles/ui/src/App/Spawner.tsx @@ -3,8 +3,6 @@ import '@patternfly/patternfly/patternfly.min.css'; import '@patternfly/patternfly/patternfly-addons.css'; import { Alert, - Button, - ButtonVariant, Title, EmptyState, EmptyStateVariant, @@ -16,12 +14,10 @@ import { WarningTriangleIcon } from '@patternfly/react-icons'; import ImageForm from '../ImageForm/ImageForm'; import SizesForm from '../SizesForm/SizesForm'; import EnvVarForm from '../EnvVarForm/EnvVarForm'; -import { HubUserRequest } from '../utils/HubCalls'; import { APIGet } from '../utils/APICalls'; import { CM_PATH, FOR_USER, INSTANCE_PATH, UI_CONFIG_PATH, USER } from '../utils/const'; import { InstanceType, UiConfigType, UserConfigMapType } from '../utils/types'; -import { initSegment } from '../utils/segmentIOUtils'; -import StartServerModal from './StartServerModal'; +import { fireTrackingEvent, initSegment } from '../utils/segmentIOUtils'; import './App.scss'; @@ -30,23 +26,9 @@ const Spawner: React.FC = () => { const [configError, setConfigError] = React.useState(); const [imageValid, setImageValid] = React.useState(false); const [userConfig, setUserConfig] = React.useState(); - const [startShown, setStartShown] = React.useState(false); - const pageRef = React.useRef(null); React.useEffect(() => { let cancelled = false; - HubUserRequest('GET') - .then((response) => { - return response?.json(); - }) - .then((results) => { - if (results.pending) { - setStartShown(true); - } - }) - .catch((e) => { - console.error(e); - }); APIGet(CM_PATH).then((data: UserConfigMapType) => { if (!cancelled) { @@ -83,6 +65,20 @@ const Spawner: React.FC = () => { }); }, []); + const fireStartServerEvent = () => { + APIGet(CM_PATH) + .then((data: UserConfigMapType) => { + fireTrackingEvent('Notebook Server Started', { + GPU: data.gpu, + lastSelectedSize: data.last_selected_size, + lastSelectedImage: data.last_selected_image, + }); + }) + .catch((e) => { + console.dir(e); + }); + }; + const renderContent = () => { if (configError) { return ( @@ -106,27 +102,30 @@ const Spawner: React.FC = () => { return ( <> - + setImageValid(true)} + /> {uiConfig.envVarConfig?.enabled !== false && ( )}
- + value="Start server" + onClick={fireStartServerEvent} + className="jsp-spawner__submit-button pf-c-button pf-m-primary" + />
); }; return ( -
+
{FOR_USER ? ( {
Select options for your notebook server.
{renderContent()} - {startShown ? ( - setStartShown(false)} - /> - ) : null}
); }; diff --git a/jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.scss b/jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.scss deleted file mode 100644 index 2532d55a..00000000 --- a/jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.scss +++ /dev/null @@ -1,88 +0,0 @@ -.jsp-spawner__start-modal { - &__progress { - margin-top: var(--pf-global--spacer--md); - } - - &__status { - margin-top: var(--pf-global--spacer--md); - } - - &__footer { - width: 100%; - } - - &__buttons { - display: flex; - gap: 10px; - margin-top: var(--pf-global--spacer--md); - width: 100%; - } - - .jsp-spawner__start-modal__accordion { - margin-top: var(--pf-global--spacer--md); - } - - .jsp-spawner__start-modal__accordion-toggle { - margin-left: calc(var(--pf-c-accordion__toggle--PaddingLeft) * -1); - margin-right: calc(var(--pf-c-accordion__toggle--PaddingRight) * -1); - width: calc(100% + var(--pf-c-accordion__toggle--PaddingLeft) + var(--pf-c-accordion__toggle--PaddingRight)); - - &.m-is-disabled { - pointer-events: none; - .pf-c-accordion__toggle-icon { - color: var(--pf-global--disabled-color--200); - } - - } - &:after { - display: none !important; - } - - &:focus, &:hover { - background-color: transparent !important; - } - } - - &__toggle-title { - color: var(--pf-c-accordion__toggle--hover__toggle-text--Color); - .m-is-disabled & { - color: var(--pf-global--disabled-color--200); - } - } - - &__accordion-body { - max-height: 250px; - overflow-y: auto; - } - - &__message { - border-top: 1px solid var(--pf-global--BorderColor--100); - margin-bottom: var(--pf-global--spacer--sm); - padding-top: var(--pf-global--spacer--sm); - word-break: break-word; - - &:first-of-type { - border-top: 0; - padding-top: 0; - } - } - - .pf-c-accordion { - &__toggle-text { - font-weight: var(--pf-global--FontWeight--normal) !important; - } - &__expanded-content { - color: var(--pf-global--Color--100); - - &-body:after { - display: none; - } - } - } - - .pf-c-modal-box__footer { - align-items: flex-start; - flex-direction: column; - gap: var(--pf-global--spacer--sm); - } -} \ No newline at end of file diff --git a/jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.tsx b/jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.tsx deleted file mode 100644 index a61597c3..00000000 --- a/jupyterhub_singleuser_profiles/ui/src/App/StartServerModal.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import * as React from 'react'; -import classNames from 'classnames'; -import { - Accordion, - AccordionItem, - AccordionToggle, - AccordionContent, - Alert, - Button, - Modal, - ModalVariant, - Progress, - ProgressVariant, -} from '@patternfly/react-core'; -import { APIGet } from '../utils/APICalls'; -import { HubUserRequest } from '../utils/HubCalls'; -import { useWatchSpawnProgress } from '../utils/useWatchSpawnProgress'; -import { CM_PATH, DEV_MODE, FOR_USER, USER } from '../utils/const'; -import { UserConfigMapType } from '../utils/types'; - -import './StartServerModal.scss'; - -type StartServerModalProps = { - shown: boolean; - onClose: () => void; - pageRef?: HTMLElement; -}; - -type SpawnStatus = { - status: 'success' | 'danger' | 'warning' | 'info' | 'default'; - title: string; - reason: React.ReactNode; -}; - -const StartServerModal: React.FC = ({ shown, onClose, pageRef }) => { - const [spawnInProgress, setSpawnInProgress] = React.useState(false); - const [expandMessages, setExpandMessages] = React.useState(false); - const spawnProgress = useWatchSpawnProgress(spawnInProgress); - const [spawnStatus, setSpawnStatus] = React.useState(null); - - const startSpawnServer = React.useCallback(() => { - setSpawnStatus(null); - - APIGet(CM_PATH).then((data: UserConfigMapType) => { - const body = JSON.stringify(data); - - HubUserRequest('POST', 'server', body) - .then((response) => { - if (DEV_MODE) { - setSpawnInProgress(true); - return; - } - if (response?.status === 202) { - HubUserRequest('GET', 'server/progress') - .then((response) => { - if (response?.status !== 400) { - setSpawnInProgress(true); - } - }) - .catch((e) => { - setSpawnStatus({ - status: 'warning', - title: 'Unable to get progress for server startup.', - reason: e.message, - }); - }); - return; - } - if (response?.status === 201) { - setSpawnStatus({ - status: 'success', - title: 'Success', - reason: 'The notebook server is up and running. This page will update momentarily.', - }); - setTimeout(() => window.location.reload(), 8000); - } - if (response?.status === 404) { - setSpawnStatus({ - status: 'danger', - title: 'Server request failed to start', - reason: ( - - User {FOR_USER || USER} does not exist - - ), - }); - setTimeout(() => window.location.reload(), 8000); - } - }) - .catch((e) => { - setSpawnStatus({ - status: 'danger', - title: 'Server request failed to start', - reason: e.message, - }); - }); - }); - }, []); - - const stopSpawnServer = React.useCallback((): Promise => { - setSpawnStatus(null); - return HubUserRequest('DELETE', 'server') - .then((res) => { - if (res?.status === 204) { - setSpawnStatus({ - status: 'default', - title: 'The server has been stopped.', - reason: '', - }); - return true; - } - setSpawnStatus({ - status: 'warning', - title: 'Server is still running.', - reason: - 'The notebook server has not yet stopped as it is taking a while to stop.\n' + - 'Please try again in a few minutes.', - }); - return false; - }) - .catch((e) => { - setSpawnStatus({ - status: 'danger', - title: 'Unable to stop current server.', - reason: 'The notebook server could not be stopped.\n' + e.message, - }); - return false; - }); - }, []); - - const retrySpawn = React.useCallback(() => { - if (DEV_MODE) { - startSpawnServer(); - } - stopSpawnServer().then((success) => { - if (success) { - startSpawnServer(); - } - }); - }, [startSpawnServer, stopSpawnServer]); - - React.useEffect(() => { - // Modal opened, Start spawning (ok to attempt to start even if already in progress) - if (shown) { - startSpawnServer(); - } - }, [shown, startSpawnServer]); - - React.useEffect(() => { - if (!spawnProgress) { - return; - } - - if (spawnProgress.failed) { - setSpawnStatus({ - status: 'danger', - title: 'Spawn failed', - reason: spawnProgress.lastMessage, - }); - setSpawnInProgress(false); - } - if (spawnProgress.ready) { - setSpawnStatus({ - status: 'success', - title: 'Success', - reason: 'The notebook server is up and running. This page will update momentarily.', - }); - setTimeout(() => window.location.reload(), 8000); - } - }, [spawnProgress]); - - const getMessageText = (message) => { - if (!message) { - return message; - } - const parts = message.split(' '); - if (parts.length < 3) { - return message; - } - const date = Date.parse(parts[0]); - if (Number.isNaN(date)) { - return message; - } - return parts.slice(2).join(' '); - }; - - const renderProgress = () => { - let variant; - switch (spawnStatus?.status) { - case 'danger': - variant = ProgressVariant.danger; - break; - case 'success': - variant = ProgressVariant.success; - break; - case 'warning': - variant = ProgressVariant.warning; - break; - default: - variant = undefined; - } - return ( -
- -
- ); - }; - - const renderStatus = () => { - if (!spawnStatus) { - return; - } - return ( -
- -

{spawnStatus.reason}

-
-
- ); - }; - - const toggleClasses = classNames('jsp-spawner__start-modal__accordion-toggle', { - 'm-is-disabled': !spawnProgress.messages.length, - }); - const renderMessages = () => { - return ( -
- - - setExpandMessages((prev) => !prev)} - isExpanded={expandMessages} - id="messages-toggle" - className={toggleClasses} - > -
- {expandMessages ? 'Collapse event log' : 'Expand event log'} -
-
- - {spawnProgress.messages.map((message, index) => ( -
- {message} -
- ))} -
-
-
-
- ); - }; - - return ( - - - Depending on the size and resources requested, this can take several minutes. To track - progress, expand the event log. - - {renderProgress()} - {renderStatus()} -
- {spawnStatus?.status === 'danger' || spawnStatus?.status === 'warning' ? ( -
- - -
- ) : null} - {renderMessages()} -
-
- ); -}; - -export default StartServerModal; diff --git a/jupyterhub_singleuser_profiles/ui/src/ImageForm/ImageForm.tsx b/jupyterhub_singleuser_profiles/ui/src/ImageForm/ImageForm.tsx index 62c9c4a4..c99b0f41 100644 --- a/jupyterhub_singleuser_profiles/ui/src/ImageForm/ImageForm.tsx +++ b/jupyterhub_singleuser_profiles/ui/src/ImageForm/ImageForm.tsx @@ -18,7 +18,7 @@ type ImageTag = { type ImageFormProps = { uiConfig: UiConfigType; userConfig: UserConfigMapType; - onValidImage?: (valid: boolean) => void; + onValidImage?: () => void; }; const getValuesFromImageName = (imageName: string): ImageTag | null => { @@ -40,7 +40,7 @@ const ImageForm: React.FC = ({ userConfig, onValidImage }) => { const postChange = React.useCallback( (text) => { const json = JSON.stringify({ last_selected_image: text }); - APIPost(CM_PATH, json).then(() => onValidImage && onValidImage(true)); + APIPost(CM_PATH, json).then(() => onValidImage && onValidImage()); }, [onValidImage], ); @@ -61,6 +61,7 @@ const ImageForm: React.FC = ({ userConfig, onValidImage }) => { if (currentImage && currentTag) { setSelectedImageTag(prevSelectedImageTag); postChange(userConfig.last_selected_image); + onValidImage && onValidImage(); return; } diff --git a/jupyterhub_singleuser_profiles/ui/src/__mock__/mockData.ts b/jupyterhub_singleuser_profiles/ui/src/__mock__/mockData.ts index 60ffb64d..d7e7d4ac 100644 --- a/jupyterhub_singleuser_profiles/ui/src/__mock__/mockData.ts +++ b/jupyterhub_singleuser_profiles/ui/src/__mock__/mockData.ts @@ -24,7 +24,6 @@ type MockDataType = { ['size/Huge']: SizeDescription; [UI_CONFIG_PATH]: UiConfigType; [DEFAULT_IMAGE_PATH]: string; - ['server/progress']: { status: number }; [USERS_PATH]: { json: () => Promise; }; @@ -482,9 +481,6 @@ export const mockData: MockDataType = { enabled: true, }, }, - ['server/progress']: { - status: 400, - }, [USERS_PATH]: { json: (): Promise => { const users: JHUser[] = []; @@ -502,107 +498,3 @@ export const mockData: MockDataType = { }, }, }; - -type SpawnUpdate = { - progress: number; - message: string; - failed: boolean; - ready: boolean; -}; - -export const MOCK_SPAWN_MESSAGES: SpawnUpdate[] = [ - { - progress: 0, - message: 'Server requested', - failed: false, - ready: false, - }, - { - progress: 10, - message: - '2021-07-29T14:17:15.651297Z [Normal] Successfully assigned redhat-ods-applications/jupyterhub-nb-rhodsadmin to ip-10-0-208-112.ec2.internal', - failed: false, - ready: false, - }, - { - progress: 20, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 30, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 40, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 50, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 60, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 70, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 80, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 90, - message: - '2021-07-29T14:17:20Z [Normal] AttachVolume.Attach succeeded for volume "pvc-e7d9d2f6-2036-4c6b-a760-9c1d60010996"', - failed: false, - ready: false, - }, - { - progress: 100, - message: 'Server request failed.', - failed: true, - ready: false, - }, -]; - -export const getMockProgress = (): EventSource => { - // eslint-disable-next-line prefer-const - let handle; - let count = 0; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const MockSource: any = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function - onmessage: (event: { data: string }) => {}, - close: () => clearTimeout(handle), - }; - - handle = setInterval(() => { - MockSource.onmessage({ data: JSON.stringify(MOCK_SPAWN_MESSAGES[count++]) }); - }, 3000); - - return MockSource as EventSource; -}; diff --git a/jupyterhub_singleuser_profiles/ui/src/index.html b/jupyterhub_singleuser_profiles/ui/src/index.html index de65ad45..3ecfb3cd 100644 --- a/jupyterhub_singleuser_profiles/ui/src/index.html +++ b/jupyterhub_singleuser_profiles/ui/src/index.html @@ -1,24 +1,4 @@ -{% extends "page.html" %} -{% if announcement_spawn %} - {% set announcement = announcement_spawn %} -{% endif %} - -{% block main %} - - - -{% if for_user and user.name != for_user.name -%} - -{% endif -%} -{% if error_message -%} - -{% endif %} + @@ -32,5 +12,3 @@
- -{% endblock %} diff --git a/jupyterhub_singleuser_profiles/ui/src/utils/HubCalls.ts b/jupyterhub_singleuser_profiles/ui/src/utils/HubCalls.ts index 22c27dda..8110b3b5 100644 --- a/jupyterhub_singleuser_profiles/ui/src/utils/HubCalls.ts +++ b/jupyterhub_singleuser_profiles/ui/src/utils/HubCalls.ts @@ -1,14 +1,13 @@ import { DEV_MODE, DEV_SERVER, - FOR_USER, HUB_PATH, MOCK_MODE, SHUTDOWN_PATH, USER, USERS_PATH, } from './const'; -import { getMockProgress, mockData } from '../__mock__/mockData'; +import { mockData } from '../__mock__/mockData'; const doSleep = (timeout: number) => { return new Promise((resolve) => setTimeout(resolve, timeout)); @@ -22,12 +21,11 @@ export const getHubPath = (request: string): string => { return hubPath; }; -export const getUserHubPath = (request?: string): string => - getHubPath(`users/${FOR_USER || USER}${request ? `/${request}` : ''}`); +export const getUserHubPath = (request: string): string => getHubPath(`users/${USER}/${request}`); export const HubUserRequest = ( method: 'GET' | 'POST' | 'DELETE', - target?: string, + target: string, json?: string, ): Promise => { const headers = { @@ -47,7 +45,7 @@ export const HubUserRequest = ( }); } return doSleep(2000).then(() => { - return mockData[target || 'user']; + return mockData[target]; }); } return new Promise((resolve, reject) => { @@ -61,14 +59,6 @@ export const HubUserRequest = ( }); }; -export const HubGetSpawnProgress = (): EventSource => { - const requestPath = getUserHubPath('server/progress'); - if (DEV_MODE) { - return getMockProgress(); - } - return new EventSource(requestPath); -}; - export const hubRequest = ( method: 'GET' | 'POST' | 'DELETE', request: string, diff --git a/jupyterhub_singleuser_profiles/ui/src/utils/useWatchSpawnProgress.tsx b/jupyterhub_singleuser_profiles/ui/src/utils/useWatchSpawnProgress.tsx deleted file mode 100644 index ff543fde..00000000 --- a/jupyterhub_singleuser_profiles/ui/src/utils/useWatchSpawnProgress.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react'; -import { HubGetSpawnProgress } from './HubCalls'; - -export type SpawnProgress = { - percentComplete: number; - messages: string[]; - lastMessage: string; - failed: boolean; - ready: boolean; -}; - -const initialProgress: SpawnProgress = { - percentComplete: 0, - messages: [], - lastMessage: '', - failed: false, - ready: false, -}; - -export const useWatchSpawnProgress = (watch: boolean): SpawnProgress => { - const [spawnProgress, setSpawnProgress] = React.useState(initialProgress); - const progressSource = React.useRef(); - - React.useEffect(() => { - if (watch) { - try { - progressSource.current = HubGetSpawnProgress(); - setSpawnProgress(initialProgress); - progressSource.current.onmessage = (event) => { - try { - const eventData = JSON.parse(event.data); - setSpawnProgress((progress) => ({ - percentComplete: eventData.progress, - messages: [...(progress?.messages || []), eventData.message], - lastMessage: eventData.message, - failed: eventData.failed, - ready: eventData.ready, - })); - if (eventData.failed || eventData.ready) { - progressSource.current?.close(); - } - } catch (e) { - console.log(`Error onmessage: `, e); - } - }; - } catch (e) { - console.log(`Error initializing progress watching: `, e); - } - } - return () => { - progressSource.current?.close(); - }; - }, [watch]); - - return spawnProgress; -}; diff --git a/jupyterhub_singleuser_profiles/ui/templates/spawn.html b/jupyterhub_singleuser_profiles/ui/templates/spawn.html new file mode 100644 index 00000000..923d3634 --- /dev/null +++ b/jupyterhub_singleuser_profiles/ui/templates/spawn.html @@ -0,0 +1,29 @@ + +{% extends "page.html" %} +{% if announcement_spawn %} + {% set announcement = announcement_spawn %} +{% endif %} + +{% block main %} + + + +{% if for_user and user.name != for_user.name -%} + +{% endif -%} +
+ {% if error_message -%} +

+ Error: {{error_message}} +

+ {% endif %} +
+ {{spawner_options_form | safe}} +
+
+ +{% endblock %}