diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx index 271304cc..86fe5f86 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx @@ -14,9 +14,14 @@ import { useNavigate } from 'react-router-dom'; import { CheckIcon } from '@patternfly/react-icons'; import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection'; import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection'; -import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection'; +import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection'; import { WorkspaceCreationPodConfigSelection } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection'; -import { WorkspaceImage, WorkspaceKind, WorkspacePodConfig } from '~/shared/types'; +import { + WorkspaceImage, + WorkspaceKind, + WorkspacePodConfig, + WorkspaceProperties, +} from '~/shared/types'; enum WorkspaceCreationSteps { KindSelection, @@ -32,6 +37,7 @@ const WorkspaceCreation: React.FunctionComponent = () => { const [selectedKind, setSelectedKind] = useState(); const [selectedImage, setSelectedImage] = useState(); const [selectedPodConfig, setSelectedPodConfig] = useState(); + const [selectedProperties, setSelectedProperties] = useState(); const getStepVariant = useCallback( (step: WorkspaceCreationSteps) => { @@ -62,6 +68,7 @@ const WorkspaceCreation: React.FunctionComponent = () => { setSelectedKind(newWorkspaceKind); setSelectedImage(undefined); setSelectedPodConfig(undefined); + setSelectedProperties(undefined) }, []); return ( @@ -155,7 +162,9 @@ const WorkspaceCreation: React.FunctionComponent = () => { /> )} {currentStep === WorkspaceCreationSteps.Properties && ( - + )} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection.tsx deleted file mode 100644 index 131425bd..00000000 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; -import { Content } from '@patternfly/react-core'; - -const WorkspaceCreationPropertiesSelection: React.FunctionComponent = () => ( - -

Configure properties for your workspace.

-
-); - -export { WorkspaceCreationPropertiesSelection }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx new file mode 100644 index 00000000..8e115286 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { useMemo, useState } from 'react'; +import { + TextInput, + Checkbox, + Form, + FormGroup, + ExpandableSection, + Divider, + Split, + SplitItem, + Content, +} from '@patternfly/react-core'; +import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails'; +import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumesProps'; +import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume } from '~/shared/types'; + +interface WorkspaceCreationImageSelectionProps { + selectedImage: WorkspaceImage | undefined; +} + +const WorkspaceCreationPropertiesSelection: React.FunctionComponent< + WorkspaceCreationImageSelectionProps +> = ({ selectedImage }) => { + const [workspaceName, setWorkspaceName] = useState(''); + const [deferUpdates, setDeferUpdates] = useState(false); + const [homeDirectory, setHomeDirectory] = useState(''); + const [volumes, setVolumes] = useState({ home: '', data: [] }); + const [volumesData, setVolumesData] = useState([]); + const [isVolumesExpanded, setIsVolumesExpanded] = useState(false); + + React.useEffect(() => { + setVolumes((prev) => ({ + ...prev, + data: volumesData, + })); + }, [volumesData]); + + const imageDetailsContent = useMemo( + () => , + [selectedImage], + ); + + return ( + +

Configure properties for your workspace.

+ + + {imageDetailsContent} + +
+
+ + setWorkspaceName(value)} + id="workspace-name" + /> + + + setDeferUpdates((prev) => !prev)} + id="defer-updates" + /> + + +
+ setIsVolumesExpanded((prev) => !prev)} + isExpanded={isVolumesExpanded} + isIndented + > + {isVolumesExpanded && ( + <> + + { + setHomeDirectory(value); + setVolumes((prev) => ({ ...prev, home: value })); + }} + id="home-directory" + /> + + + + + + + )} + +
+ {!isVolumesExpanded && ( +
+
Workspace volumes enable your project data to persist.
+
+ {volumes.data.length} added +
+
+ )} + +
+
+
+
+ ); +}; + +export { WorkspaceCreationPropertiesSelection }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumesProps.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumesProps.tsx new file mode 100644 index 00000000..40403fc0 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumesProps.tsx @@ -0,0 +1,205 @@ +import React, { useCallback, useState } from 'react'; +import { EllipsisVIcon } from '@patternfly/react-icons'; +import { Table, Thead, Tbody, Tr, Th, Td, TableVariant } from '@patternfly/react-table'; +import { + Button, + Modal, + ModalVariant, + TextInput, + Switch, + Dropdown, + DropdownItem, + MenuToggle, + ModalBody, + ModalFooter, + Form, + FormGroup, + ModalHeader, +} from '@patternfly/react-core'; +import { WorkspaceVolume } from '~/shared/types'; + +interface WorkspaceCreationPropertiesVolumesProps { + volumes: WorkspaceVolume[]; + setVolumes: React.Dispatch>; +} + +export const WorkspaceCreationPropertiesVolumes: React.FC< + WorkspaceCreationPropertiesVolumesProps +> = ({ volumes, setVolumes }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [formData, setFormData] = useState({ pvcName: '', mountPath: '', readOnly: false }); + const [editIndex, setEditIndex] = useState(null); + const [deleteIndex, setDeleteIndex] = useState(null); + const [dropdownOpen, setDropdownOpen] = useState(null); + + const resetForm = useCallback(() => { + setFormData({ pvcName: '', mountPath: '', readOnly: false }); + setEditIndex(null); + setIsModalOpen(false); + }, []); + + const handleAddOrEdit = useCallback(() => { + if (!formData.pvcName || !formData.mountPath) { + return; + } + if (editIndex !== null) { + const updated = [...volumes]; + updated[editIndex] = formData; + setVolumes(updated); + } else { + setVolumes([...volumes, formData]); + } + resetForm(); + }, [formData, editIndex, volumes, setVolumes, resetForm]); + + const handleEdit = useCallback( + (index: number) => { + setFormData(volumes[index]); + setEditIndex(index); + setIsModalOpen(true); + }, + [volumes], + ); + + const openDetachModal = useCallback((index: number) => { + setDeleteIndex(index); + setIsDeleteModalOpen(true); + }, []); + + const handleDelete = useCallback(() => { + if (deleteIndex !== null) { + setVolumes(volumes.filter((_, i) => i !== deleteIndex)); + setIsDeleteModalOpen(false); + setDeleteIndex(null); + } + }, [deleteIndex, volumes, setVolumes]); + + return ( + <> + {volumes.length > 0 && ( + + + + + + + + + + {volumes.map((volume, index) => ( + + + + + + + ))} + +
PVC NameMount PathRead Only +
{volume.pvcName}{volume.mountPath} + { + const updated = [...volumes]; + updated[index].readOnly = !updated[index].readOnly; + setVolumes(updated); + }} + /> + + ( + setDropdownOpen(dropdownOpen === index ? null : index)} + variant="plain" + aria-label="plain kebab" + > + + + )} + isOpen={dropdownOpen === index} + onSelect={() => setDropdownOpen(null)} + popperProps={{ position: 'right' }} + > + handleEdit(index)}>Edit + openDetachModal(index)}>Detach + +
+ )} + + + + + +
+ + setFormData({ ...formData, pvcName: val })} + id="pvc-name" + /> + + + setFormData({ ...formData, mountPath: val })} + id="mount-path" + /> + +
+
+ + + + +
+ setIsDeleteModalOpen(false)} + variant={ModalVariant.small} + > + + + + + + + + ); +}; + +export default WorkspaceCreationPropertiesVolumesProps; diff --git a/workspaces/frontend/src/shared/style/MUI-theme.scss b/workspaces/frontend/src/shared/style/MUI-theme.scss index c983407b..8fb14129 100644 --- a/workspaces/frontend/src/shared/style/MUI-theme.scss +++ b/workspaces/frontend/src/shared/style/MUI-theme.scss @@ -751,8 +751,8 @@ } .mui-theme .pf-v6-c-modal-box { - --pf-v6-c-modal-box--BorderRadius: 0; - border: 2px solid var(--mui-palette-common-black); + --pf-v6-c-modal-box--BorderRadius: var(--mui-shape-borderRadius); + --pf-v6-c-modal-box--BoxShadow: var(--mui-shadows-24); } .mui-theme .pf-v6-c-button.pf-m-plain { diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index a88fe8ad..8249ac80 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -20,6 +20,32 @@ export interface WorkspaceImage { }; } +export interface WorkspaceVolume { + pvcName: string; + mountPath: string; + readOnly: boolean; +} + +export interface WorkspaceVolumes { + home: string; + data: WorkspaceVolume[]; +} + +export interface WorkspaceProperties { + workspaceName: string; + deferUpdates: boolean; + homeDirectory: string; + volumes: boolean; + isVolumesExpanded: boolean + redirect?: { + to: string; + message: { + text: string; + level: string; + }; + }; +} + export interface WorkspacePodConfig { id: string; displayName: string; @@ -106,14 +132,7 @@ export interface Workspace { labels: string[]; annotations: string[]; }; - volumes: { - home: string; - data: { - pvcName: string; - mountPath: string; - readOnly: boolean; - }[]; - }; + volumes: WorkspaceVolumes; endpoints: { displayName: string; port: string;