From 3218768df032cc173d91933e8c8a7ab5ff5eb229 Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:08:58 -0400 Subject: [PATCH] fix(ws): Implement dual scrolling for workspace kind wizard (#484) Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> fix(ws): remove extra DrawerPanelBody remove unused file Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> fix(ws): remove comment and hide drawer on previousStep callback Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> fix(ws): when navigating between wizard steps, show drawer for steps that have drawer content --- .../pages/Workspaces/Form/WorkspaceForm.tsx | 414 +++++++++++------- .../Form/image/WorkspaceFormImageDetails.tsx | 4 +- .../image/WorkspaceFormImageSelection.tsx | 59 +-- .../Form/kind/WorkspaceFormKindDetails.tsx | 4 +- .../Form/kind/WorkspaceFormKindSelection.tsx | 47 +- .../WorkspaceFormPodConfigDetails.tsx | 6 +- .../WorkspaceFormPodConfigSelection.tsx | 59 +-- 7 files changed, 301 insertions(+), 292 deletions(-) diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx index 89a29825..78d4d709 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx @@ -2,14 +2,21 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Content, + Drawer, + DrawerContent, + DrawerContentBody, + DrawerPanelContent, + DrawerHead, + DrawerActions, + DrawerCloseButton, + DrawerPanelBody, Flex, FlexItem, - PageGroup, PageSection, ProgressStep, ProgressStepper, Stack, - StackItem, + Title, } from '@patternfly/react-core'; import useGenericObjectState from '~/app/hooks/useGenericObjectState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; @@ -18,10 +25,18 @@ import { WorkspaceFormKindSelection } from '~/app/pages/Workspaces/Form/kind/Wor import { WorkspaceFormPodConfigSelection } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection'; import { WorkspaceFormPropertiesSelection } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection'; import { WorkspaceFormData } from '~/app/types'; -import { WorkspaceCreate } from '~/shared/api/backendApiTypes'; +import { + WorkspaceCreate, + WorkspaceKind, + WorkspaceImageConfigValue, + WorkspacePodConfigValue, +} from '~/shared/api/backendApiTypes'; import useWorkspaceFormData from '~/app/hooks/useWorkspaceFormData'; import { useTypedNavigate } from '~/app/routerHelper'; import { useWorkspaceFormLocationData } from '~/app/hooks/useWorkspaceFormLocationData'; +import { WorkspaceFormKindDetails } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails'; +import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails'; +import { WorkspaceFormPodConfigDetails } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails'; enum WorkspaceFormSteps { KindSelection, @@ -52,6 +67,7 @@ const WorkspaceForm: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false); const [currentStep, setCurrentStep] = useState(WorkspaceFormSteps.KindSelection); + const [drawerExpanded, setDrawerExpanded] = useState(false); const [data, setData, resetData, replaceData] = useGenericObjectState(initialFormData); @@ -76,30 +92,46 @@ const WorkspaceForm: React.FC = () => { [currentStep], ); + const isStepValid = useCallback( + (step: WorkspaceFormSteps) => { + switch (step) { + case WorkspaceFormSteps.KindSelection: + return !!data.kind; + case WorkspaceFormSteps.ImageSelection: + return !!data.image; + case WorkspaceFormSteps.PodConfigSelection: + return !!data.podConfig; + case WorkspaceFormSteps.Properties: + return !!data.properties.workspaceName.trim(); + default: + return false; + } + }, + [data.kind, data.image, data.podConfig, data.properties.workspaceName], + ); + + const showDrawer = useCallback( + (step: WorkspaceFormSteps) => + // Only show drawer for steps that have drawer content + step !== WorkspaceFormSteps.Properties && isStepValid(step), + [isStepValid], + ); + const previousStep = useCallback(() => { - setCurrentStep(currentStep - 1); - }, [currentStep]); + const newStep = currentStep - 1; + setCurrentStep(newStep); + setDrawerExpanded(showDrawer(newStep)); + }, [currentStep, showDrawer]); const nextStep = useCallback(() => { - setCurrentStep(currentStep + 1); - }, [currentStep]); + const newStep = currentStep + 1; + setCurrentStep(newStep); + setDrawerExpanded(showDrawer(newStep)); + }, [currentStep, showDrawer]); const canGoToPreviousStep = useMemo(() => currentStep > 0, [currentStep]); - const isCurrentStepValid = useMemo(() => { - switch (currentStep) { - case WorkspaceFormSteps.KindSelection: - return !!data.kind; - case WorkspaceFormSteps.ImageSelection: - return !!data.image; - case WorkspaceFormSteps.PodConfigSelection: - return !!data.podConfig; - case WorkspaceFormSteps.Properties: - return !!data.properties.workspaceName.trim(); - default: - return false; - } - }, [currentStep, data]); + const isCurrentStepValid = useMemo(() => isStepValid(currentStep), [isStepValid, currentStep]); const canGoToNextStep = useMemo( () => currentStep < Object.keys(WorkspaceFormSteps).length / 2 - 1, @@ -168,6 +200,63 @@ const WorkspaceForm: React.FC = () => { navigate('workspaces'); }, [navigate]); + const handleKindSelect = useCallback( + (kind: WorkspaceKind | undefined) => { + if (kind) { + resetData(); + setData('kind', kind); + setDrawerExpanded(true); + } + }, + [resetData, setData], + ); + + const handleImageSelect = useCallback( + (image: WorkspaceImageConfigValue | undefined) => { + if (image) { + setData('image', image); + setDrawerExpanded(true); + } + }, + [setData], + ); + + const handlePodConfigSelect = useCallback( + (podConfig: WorkspacePodConfigValue | undefined) => { + if (podConfig) { + setData('podConfig', podConfig); + setDrawerExpanded(true); + } + }, + [setData], + ); + + const getDrawerContent = () => { + switch (currentStep) { + case WorkspaceFormSteps.KindSelection: + return ; + case WorkspaceFormSteps.ImageSelection: + return ; + case WorkspaceFormSteps.PodConfigSelection: + return ; + default: + return null; + } + }; + + const getDrawerTitle = () => { + switch (currentStep) { + case WorkspaceFormSteps.KindSelection: + return 'Workspace Kind'; + case WorkspaceFormSteps.ImageSelection: + return 'Image'; + case WorkspaceFormSteps.PodConfigSelection: + return 'Pod Config'; + default: + return ''; + } + }; + if (initialFormDataError) { return

Error loading workspace data: {initialFormDataError.message}

; // TODO: UX for error state } @@ -176,137 +265,160 @@ const WorkspaceForm: React.FC = () => { return

Loading...

; // TODO: UX for loading state } + const panelContent = ( + + + {getDrawerTitle()} + + setDrawerExpanded(false)} /> + + + + {getDrawerContent()} + + + ); + return ( - <> - - - - - - -

{`${mode === 'create' ? 'Create' : 'Edit'} workspace`}

-
-
- - - - Workspace Kind - - - Image - - - Pod Config - - - Properties - - - -
- -

{stepDescriptions[currentStep]}

-
-
-
-
- - {currentStep === WorkspaceFormSteps.KindSelection && ( - { - resetData(); - setData('kind', kind); - }} - /> - )} - {currentStep === WorkspaceFormSteps.ImageSelection && ( - setData('image', image)} - images={data.kind?.podTemplate.options.imageConfig.values ?? []} - /> - )} - {currentStep === WorkspaceFormSteps.PodConfigSelection && ( - setData('podConfig', podConfig)} - podConfigs={data.kind?.podTemplate.options.podConfig.values ?? []} - /> - )} - {currentStep === WorkspaceFormSteps.Properties && ( - setData('properties', properties)} - selectedImage={data.image} - /> - )} - - - - - - - - {canGoToNextStep ? ( - - ) : ( - - )} - - - - - - - + + + + + + + + + + +

{`${mode === 'create' ? 'Create' : 'Edit'} workspace`}

+

{stepDescriptions[currentStep]}

+
+
+ + + + Workspace Kind + + + Image + + + Pod Config + + + Properties + + + +
+
+
+
+ + + {currentStep === WorkspaceFormSteps.KindSelection && ( + + )} + {currentStep === WorkspaceFormSteps.ImageSelection && ( + + )} + {currentStep === WorkspaceFormSteps.PodConfigSelection && ( + + )} + {currentStep === WorkspaceFormSteps.Properties && ( + setData('properties', properties)} + selectedImage={data.image} + /> + )} + + + + + + + + + + {canGoToNextStep ? ( + + ) : ( + + )} + + + + + + + +
+
+
+
); }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx index 26221492..1b6eb14d 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx @@ -16,7 +16,7 @@ type WorkspaceFormImageDetailsProps = { export const WorkspaceFormImageDetails: React.FunctionComponent = ({ workspaceImage, }) => ( -
+ <> {workspaceImage && ( <> {workspaceImage.displayName} @@ -38,5 +38,5 @@ export const WorkspaceFormImageDetails: React.FunctionComponent )} -
+ ); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx index 83ac839a..02fc9556 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx @@ -1,10 +1,8 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Content, Split, SplitItem } from '@patternfly/react-core'; -import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails'; import { WorkspaceFormImageList } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageList'; import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels'; import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; -import { WorkspaceFormDrawer } from '~/app/pages/Workspaces/Form/WorkspaceFormDrawer'; interface WorkspaceFormImageSelectionProps { images: WorkspaceImageConfigValue[]; @@ -18,26 +16,6 @@ const WorkspaceFormImageSelection: React.FunctionComponent { const [selectedLabels, setSelectedLabels] = useState>>(new Map()); - const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); - - const onExpand = useCallback(() => { - if (drawerRef.current) { - drawerRef.current.focus(); - } - }, []); - - const onClick = useCallback( - (image?: WorkspaceImageConfigValue) => { - setIsExpanded(true); - onSelect(image); - }, - [onSelect], - ); - - const onCloseClick = useCallback(() => { - setIsExpanded(false); - }, []); const imageFilterContent = useMemo( () => ( @@ -50,32 +28,19 @@ const WorkspaceFormImageSelection: React.FunctionComponent , - [selectedImage], - ); - return ( - - - {imageFilterContent} - - - - - + + {imageFilterContent} + + + + ); }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx index 6edbf7b0..105a4890 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx @@ -9,12 +9,12 @@ type WorkspaceFormKindDetailsProps = { export const WorkspaceFormKindDetails: React.FunctionComponent = ({ workspaceKind, }) => ( -
+ <> {workspaceKind && ( <> {workspaceKind.displayName}

{workspaceKind.description}

)} -
+ ); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx index 342a4825..17bf4dd8 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx @@ -1,10 +1,8 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React from 'react'; import { Content } from '@patternfly/react-core'; import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; -import { WorkspaceFormKindDetails } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails'; import { WorkspaceFormKindList } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindList'; -import { WorkspaceFormDrawer } from '~/app/pages/Workspaces/Form/WorkspaceFormDrawer'; interface WorkspaceFormKindSelectionProps { selectedKind: WorkspaceKind | undefined; @@ -16,31 +14,6 @@ const WorkspaceFormKindSelection: React.FunctionComponent { const [workspaceKinds, loaded, error] = useWorkspaceKinds(); - const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); - - const onExpand = useCallback(() => { - if (drawerRef.current) { - drawerRef.current.focus(); - } - }, []); - - const onClick = useCallback( - (kind?: WorkspaceKind) => { - setIsExpanded(true); - onSelect(kind); - }, - [onSelect], - ); - - const onCloseClick = useCallback(() => { - setIsExpanded(false); - }, []); - - const kindDetailsContent = useMemo( - () => , - [selectedKind], - ); if (error) { return

Error loading workspace kinds: {error.message}

; // TODO: UX for error state @@ -52,19 +25,11 @@ const WorkspaceFormKindSelection: React.FunctionComponent - - - + ); }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx index dee1dd20..bf7f3fb5 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx @@ -19,10 +19,12 @@ export const WorkspaceFormPodConfigDetails: React.FunctionComponent< > = ({ workspacePodConfig }) => ( <> {workspacePodConfig && ( -
+ <> {workspacePodConfig.displayName}{' '}

{workspacePodConfig.description}

+
+
{workspacePodConfig.labels.map((label) => ( ))} -
+ )} ); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx index d3c04744..2aac1cff 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx @@ -1,9 +1,7 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Content, Split, SplitItem } from '@patternfly/react-core'; -import { WorkspaceFormPodConfigDetails } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails'; import { WorkspaceFormPodConfigList } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList'; import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels'; -import { WorkspaceFormDrawer } from '~/app/pages/Workspaces/Form/WorkspaceFormDrawer'; import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; interface WorkspaceFormPodConfigSelectionProps { @@ -16,26 +14,6 @@ const WorkspaceFormPodConfigSelection: React.FunctionComponent< WorkspaceFormPodConfigSelectionProps > = ({ podConfigs, selectedPodConfig, onSelect }) => { const [selectedLabels, setSelectedLabels] = useState>>(new Map()); - const [isExpanded, setIsExpanded] = useState(false); - const drawerRef = useRef(undefined); - - const onExpand = useCallback(() => { - if (drawerRef.current) { - drawerRef.current.focus(); - } - }, []); - - const onClick = useCallback( - (podConfig?: WorkspacePodConfigValue) => { - setIsExpanded(true); - onSelect(podConfig); - }, - [onSelect], - ); - - const onCloseClick = useCallback(() => { - setIsExpanded(false); - }, []); const podConfigFilterContent = useMemo( () => ( @@ -48,32 +26,19 @@ const WorkspaceFormPodConfigSelection: React.FunctionComponent< [podConfigs, selectedLabels, setSelectedLabels], ); - const podConfigDetailsContent = useMemo( - () => , - [selectedPodConfig], - ); - return ( - - - {podConfigFilterContent} - - - - - + + {podConfigFilterContent} + + + + ); };