-
Notifications
You must be signed in to change notification settings - Fork 40
feat(ws): Add properties tile to new workspace creation #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: notebooks-v2
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<WorkspaceVolumes>({ home: '', data: [] }); | ||
const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]); | ||
const [isVolumesExpanded, setIsVolumesExpanded] = useState(false); | ||
|
||
React.useEffect(() => { | ||
setVolumes((prev) => ({ | ||
...prev, | ||
data: volumesData, | ||
})); | ||
}, [volumesData]); | ||
|
||
const imageDetailsContent = useMemo( | ||
() => <WorkspaceCreationImageDetails workspaceImage={selectedImage} />, | ||
[selectedImage], | ||
); | ||
|
||
return ( | ||
<Content style={{ height: '100%' }}> | ||
<p>Configure properties for your workspace.</p> | ||
<Divider /> | ||
<Split hasGutter> | ||
<SplitItem style={{ minWidth: '200px' }}>{imageDetailsContent}</SplitItem> | ||
<SplitItem isFilled> | ||
<div className="pf-u-p-lg pf-u-max-width-xl"> | ||
<Form> | ||
<FormGroup | ||
label="Workspace Name" | ||
isRequired | ||
fieldId="workspace-name" | ||
style={{ width: 520 }} | ||
> | ||
<TextInput | ||
isRequired | ||
type="text" | ||
value={workspaceName} | ||
onChange={(_, value) => setWorkspaceName(value)} | ||
id="workspace-name" | ||
/> | ||
</FormGroup> | ||
<FormGroup fieldId="defer-updates"> | ||
<Checkbox | ||
label="Defer Updates" | ||
isChecked={deferUpdates} | ||
onChange={() => setDeferUpdates((prev) => !prev)} | ||
id="defer-updates" | ||
/> | ||
</FormGroup> | ||
<Divider /> | ||
<div className="pf-u-mb-0"> | ||
<ExpandableSection | ||
toggleText="Volumes" | ||
onToggle={() => setIsVolumesExpanded((prev) => !prev)} | ||
isExpanded={isVolumesExpanded} | ||
isIndented | ||
> | ||
{isVolumesExpanded && ( | ||
<> | ||
<FormGroup | ||
label="Home Directory" | ||
fieldId="home-directory" | ||
style={{ width: 500 }} | ||
> | ||
<TextInput | ||
value={homeDirectory} | ||
onChange={(_, value) => { | ||
setHomeDirectory(value); | ||
setVolumes((prev) => ({ ...prev, home: value })); | ||
}} | ||
id="home-directory" | ||
/> | ||
</FormGroup> | ||
|
||
<FormGroup fieldId="volumes-table" style={{ marginTop: '1rem' }}> | ||
<WorkspaceCreationPropertiesVolumes | ||
volumes={volumesData} | ||
setVolumes={setVolumesData} | ||
/> | ||
</FormGroup> | ||
</> | ||
)} | ||
</ExpandableSection> | ||
</div> | ||
{!isVolumesExpanded && ( | ||
<div style={{ paddingLeft: '36px', marginTop: '-10px' }}> | ||
<div>Workspace volumes enable your project data to persist.</div> | ||
<div className="pf-u-font-size-sm"> | ||
<strong>{volumes.data.length} added</strong> | ||
</div> | ||
</div> | ||
)} | ||
</Form> | ||
</div> | ||
</SplitItem> | ||
</Split> | ||
</Content> | ||
); | ||
}; | ||
|
||
export { WorkspaceCreationPropertiesSelection }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<React.SetStateAction<WorkspaceVolume[]>>; | ||
} | ||
|
||
export const WorkspaceCreationPropertiesVolumes: React.FC< | ||
WorkspaceCreationPropertiesVolumesProps | ||
> = ({ volumes, setVolumes }) => { | ||
const [isModalOpen, setIsModalOpen] = useState(false); | ||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); | ||
const [formData, setFormData] = useState<WorkspaceVolume>({ pvcName: '', mountPath: '', readOnly: false }); | ||
Check failure on line 31 in workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumesProps.tsx
|
||
const [editIndex, setEditIndex] = useState<number | null>(null); | ||
const [deleteIndex, setDeleteIndex] = useState<number | null>(null); | ||
const [dropdownOpen, setDropdownOpen] = useState<number | null>(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 && ( | ||
<Table variant={TableVariant.compact} aria-label="Volumes Table"> | ||
<Thead> | ||
<Tr> | ||
<Th>PVC Name</Th> | ||
<Th>Mount Path</Th> | ||
<Th>Read Only</Th> | ||
<Th /> | ||
</Tr> | ||
</Thead> | ||
<Tbody> | ||
{volumes.map((volume, index) => ( | ||
<Tr key={index}> | ||
<Td>{volume.pvcName}</Td> | ||
<Td>{volume.mountPath}</Td> | ||
<Td> | ||
<Switch | ||
id={`readonly-switch-${index}`} | ||
isChecked={volume.readOnly} | ||
onChange={() => { | ||
const updated = [...volumes]; | ||
updated[index].readOnly = !updated[index].readOnly; | ||
setVolumes(updated); | ||
}} | ||
/> | ||
</Td> | ||
<Td isActionCell> | ||
<Dropdown | ||
toggle={(toggleRef) => ( | ||
<MenuToggle | ||
ref={toggleRef} | ||
isExpanded={dropdownOpen === index} | ||
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)} | ||
variant="plain" | ||
aria-label="plain kebab" | ||
> | ||
<EllipsisVIcon /> | ||
</MenuToggle> | ||
)} | ||
isOpen={dropdownOpen === index} | ||
onSelect={() => setDropdownOpen(null)} | ||
popperProps={{ position: 'right' }} | ||
> | ||
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem> | ||
<DropdownItem onClick={() => openDetachModal(index)}>Detach</DropdownItem> | ||
</Dropdown> | ||
</Td> | ||
</Tr> | ||
))} | ||
</Tbody> | ||
</Table> | ||
)} | ||
<Button | ||
variant="primary" | ||
onClick={() => setIsModalOpen(true)} | ||
style={{ marginTop: '1rem' }} | ||
className="pf-u-mt-md" | ||
> | ||
Create Volume | ||
</Button> | ||
|
||
<Modal isOpen={isModalOpen} onClose={resetForm} variant={ModalVariant.small}> | ||
<ModalHeader | ||
title={editIndex !== null ? 'Edit Volume' : 'Create Volume'} | ||
description="Add a volume and optionally connect it with an existing workspace." | ||
/> | ||
<ModalBody> | ||
<Form> | ||
<FormGroup label="PVC Name" isRequired fieldId="pvc-name"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO the volume creation modal should allow the user to set the volume as read-only with the slider component, like in the table. |
||
<TextInput | ||
name="pvcName" | ||
isRequired | ||
type="text" | ||
value={formData.pvcName} | ||
onChange={(_, val) => setFormData({ ...formData, pvcName: val })} | ||
id="pvc-name" | ||
/> | ||
</FormGroup> | ||
<FormGroup label="Mount Path" isRequired fieldId="mount-path"> | ||
<TextInput | ||
name="mountPath" | ||
isRequired | ||
type="text" | ||
value={formData.mountPath} | ||
onChange={(_, val) => setFormData({ ...formData, mountPath: val })} | ||
id="mount-path" | ||
/> | ||
</FormGroup> | ||
</Form> | ||
</ModalBody> | ||
<ModalFooter> | ||
<Button | ||
key="confirm" | ||
onClick={handleAddOrEdit} | ||
isDisabled={!formData.pvcName || !formData.mountPath} | ||
> | ||
{editIndex !== null ? 'Save' : 'Create'} | ||
</Button> | ||
<Button key="cancel" variant="link" onClick={resetForm}> | ||
Cancel | ||
</Button> | ||
</ModalFooter> | ||
</Modal> | ||
<Modal | ||
isOpen={isDeleteModalOpen} | ||
onClose={() => setIsDeleteModalOpen(false)} | ||
variant={ModalVariant.small} | ||
> | ||
<ModalHeader | ||
title="Detach Volume?" | ||
description="The volume and all of its resources will be detached from the workspace." | ||
/> | ||
<ModalFooter> | ||
<Button key="detach" variant="danger" onClick={handleDelete}> | ||
Detach | ||
</Button> | ||
<Button key="cancel" variant="link" onClick={() => setIsDeleteModalOpen(false)}> | ||
Cancel | ||
</Button> | ||
</ModalFooter> | ||
</Modal> | ||
</> | ||
); | ||
}; | ||
|
||
export default WorkspaceCreationPropertiesVolumesProps; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file should be called
WorkspaceCreationPropertiesVolumes.tsx
.