Skip to content

Commit c3a6f54

Browse files
henschwartzHen Schwartz (EXT-Nokia)
andauthored
feat(ws): Add properties tile to new workspace creation (#262)
* feat(ws): Add properties tile to new workspace creation Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com> * feat(ws): Add properties tile to new workspace creation Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com> * feat(ws): Add properties tile to new workspace creation Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com> * feat(ws): Add properties tile to new workspace creation Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com> --------- Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com> Co-authored-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>
1 parent 1e82805 commit c3a6f54

File tree

6 files changed

+372
-23
lines changed

6 files changed

+372
-23
lines changed

workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import { useNavigate } from 'react-router-dom';
1616
import { CheckIcon } from '@patternfly/react-icons';
1717
import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection';
1818
import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection';
19-
import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection';
19+
import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection';
2020
import { WorkspaceCreationPodConfigSelection } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection';
21-
import { WorkspaceImage, WorkspaceKind, WorkspacePodConfig } from '~/shared/types';
21+
import {
22+
WorkspaceImage,
23+
WorkspaceKind,
24+
WorkspacePodConfig,
25+
WorkspaceProperties,
26+
} from '~/shared/types';
2227

2328
enum WorkspaceCreationSteps {
2429
KindSelection,
@@ -34,6 +39,7 @@ const WorkspaceCreation: React.FunctionComponent = () => {
3439
const [selectedKind, setSelectedKind] = useState<WorkspaceKind | undefined>();
3540
const [selectedImage, setSelectedImage] = useState<WorkspaceImage | undefined>();
3641
const [selectedPodConfig, setSelectedPodConfig] = useState<WorkspacePodConfig | undefined>();
42+
const [, setSelectedProperties] = useState<WorkspaceProperties | undefined>();
3743

3844
const getStepVariant = useCallback(
3945
(step: WorkspaceCreationSteps) => {
@@ -64,6 +70,7 @@ const WorkspaceCreation: React.FunctionComponent = () => {
6470
setSelectedKind(newWorkspaceKind);
6571
setSelectedImage(undefined);
6672
setSelectedPodConfig(undefined);
73+
setSelectedProperties(undefined);
6774
}, []);
6875

6976
return (
@@ -169,7 +176,7 @@ const WorkspaceCreation: React.FunctionComponent = () => {
169176
/>
170177
)}
171178
{currentStep === WorkspaceCreationSteps.Properties && (
172-
<WorkspaceCreationPropertiesSelection />
179+
<WorkspaceCreationPropertiesSelection selectedImage={selectedImage} />
173180
)}
174181
</PageSection>
175182
<PageSection isFilled={false} stickyOnBreakpoint={{ default: 'bottom' }}>

workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as React from 'react';
2+
import { useEffect, useMemo, useState } from 'react';
3+
import {
4+
TextInput,
5+
Checkbox,
6+
Form,
7+
FormGroup,
8+
ExpandableSection,
9+
Divider,
10+
Split,
11+
SplitItem,
12+
Content,
13+
} from '@patternfly/react-core';
14+
import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails';
15+
import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes';
16+
import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume } from '~/shared/types';
17+
18+
interface WorkspaceCreationPropertiesSelectionProps {
19+
selectedImage: WorkspaceImage | undefined;
20+
}
21+
22+
const WorkspaceCreationPropertiesSelection: React.FunctionComponent<
23+
WorkspaceCreationPropertiesSelectionProps
24+
> = ({ selectedImage }) => {
25+
const [workspaceName, setWorkspaceName] = useState('');
26+
const [deferUpdates, setDeferUpdates] = useState(false);
27+
const [homeDirectory, setHomeDirectory] = useState('');
28+
const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [] });
29+
const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]);
30+
const [isVolumesExpanded, setIsVolumesExpanded] = useState(false);
31+
32+
useEffect(() => {
33+
setVolumes((prev) => ({
34+
...prev,
35+
data: volumesData,
36+
}));
37+
}, [volumesData]);
38+
39+
const imageDetailsContent = useMemo(
40+
() => <WorkspaceCreationImageDetails workspaceImage={selectedImage} />,
41+
[selectedImage],
42+
);
43+
44+
return (
45+
<Content style={{ height: '100%' }}>
46+
<p>Configure properties for your workspace.</p>
47+
<Divider />
48+
<Split hasGutter>
49+
<SplitItem style={{ minWidth: '200px' }}>{imageDetailsContent}</SplitItem>
50+
<SplitItem isFilled>
51+
<div className="pf-u-p-lg pf-u-max-width-xl">
52+
<Form>
53+
<FormGroup
54+
label="Workspace Name"
55+
isRequired
56+
fieldId="workspace-name"
57+
style={{ width: 520 }}
58+
>
59+
<TextInput
60+
isRequired
61+
type="text"
62+
value={workspaceName}
63+
onChange={(_, value) => setWorkspaceName(value)}
64+
id="workspace-name"
65+
/>
66+
</FormGroup>
67+
<FormGroup fieldId="defer-updates">
68+
<Checkbox
69+
label="Defer Updates"
70+
isChecked={deferUpdates}
71+
onChange={() => setDeferUpdates((prev) => !prev)}
72+
id="defer-updates"
73+
/>
74+
</FormGroup>
75+
<Divider />
76+
<div className="pf-u-mb-0">
77+
<ExpandableSection
78+
toggleText="Volumes"
79+
onToggle={() => setIsVolumesExpanded((prev) => !prev)}
80+
isExpanded={isVolumesExpanded}
81+
isIndented
82+
>
83+
{isVolumesExpanded && (
84+
<>
85+
<FormGroup
86+
label="Home Directory"
87+
fieldId="home-directory"
88+
style={{ width: 500 }}
89+
>
90+
<TextInput
91+
value={homeDirectory}
92+
onChange={(_, value) => {
93+
setHomeDirectory(value);
94+
setVolumes((prev) => ({ ...prev, home: value }));
95+
}}
96+
id="home-directory"
97+
/>
98+
</FormGroup>
99+
100+
<FormGroup fieldId="volumes-table" style={{ marginTop: '1rem' }}>
101+
<WorkspaceCreationPropertiesVolumes
102+
volumes={volumesData}
103+
setVolumes={setVolumesData}
104+
/>
105+
</FormGroup>
106+
</>
107+
)}
108+
</ExpandableSection>
109+
</div>
110+
{!isVolumesExpanded && (
111+
<div style={{ paddingLeft: '36px', marginTop: '-10px' }}>
112+
<div>Workspace volumes enable your project data to persist.</div>
113+
<div className="pf-u-font-size-sm">
114+
<strong>{volumes.data.length} added</strong>
115+
</div>
116+
</div>
117+
)}
118+
</Form>
119+
</div>
120+
</SplitItem>
121+
</Split>
122+
</Content>
123+
);
124+
};
125+
126+
export { WorkspaceCreationPropertiesSelection };
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import React, { useCallback, useState } from 'react';
2+
import { EllipsisVIcon } from '@patternfly/react-icons';
3+
import { Table, Thead, Tbody, Tr, Th, Td, TableVariant } from '@patternfly/react-table';
4+
import {
5+
Button,
6+
Modal,
7+
ModalVariant,
8+
TextInput,
9+
Switch,
10+
Dropdown,
11+
DropdownItem,
12+
MenuToggle,
13+
ModalBody,
14+
ModalFooter,
15+
Form,
16+
FormGroup,
17+
ModalHeader,
18+
} from '@patternfly/react-core';
19+
import { WorkspaceVolume } from '~/shared/types';
20+
21+
interface WorkspaceCreationPropertiesVolumesProps {
22+
volumes: WorkspaceVolume[];
23+
setVolumes: React.Dispatch<React.SetStateAction<WorkspaceVolume[]>>;
24+
}
25+
26+
export const WorkspaceCreationPropertiesVolumes: React.FC<
27+
WorkspaceCreationPropertiesVolumesProps
28+
> = ({ volumes, setVolumes }) => {
29+
const [isModalOpen, setIsModalOpen] = useState(false);
30+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
31+
const [formData, setFormData] = useState<WorkspaceVolume>({
32+
pvcName: '',
33+
mountPath: '',
34+
readOnly: false,
35+
});
36+
const [editIndex, setEditIndex] = useState<number | null>(null);
37+
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
38+
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
39+
40+
const resetForm = useCallback(() => {
41+
setFormData({ pvcName: '', mountPath: '', readOnly: false });
42+
setEditIndex(null);
43+
setIsModalOpen(false);
44+
}, []);
45+
46+
const handleAddOrEdit = useCallback(() => {
47+
if (!formData.pvcName || !formData.mountPath) {
48+
return;
49+
}
50+
if (editIndex !== null) {
51+
const updated = [...volumes];
52+
updated[editIndex] = formData;
53+
setVolumes(updated);
54+
} else {
55+
setVolumes([...volumes, formData]);
56+
}
57+
resetForm();
58+
}, [formData, editIndex, volumes, setVolumes, resetForm]);
59+
60+
const handleEdit = useCallback(
61+
(index: number) => {
62+
setFormData(volumes[index]);
63+
setEditIndex(index);
64+
setIsModalOpen(true);
65+
},
66+
[volumes],
67+
);
68+
69+
const openDetachModal = useCallback((index: number) => {
70+
setDeleteIndex(index);
71+
setIsDeleteModalOpen(true);
72+
}, []);
73+
74+
const handleDelete = useCallback(() => {
75+
if (deleteIndex !== null) {
76+
setVolumes(volumes.filter((_, i) => i !== deleteIndex));
77+
setIsDeleteModalOpen(false);
78+
setDeleteIndex(null);
79+
}
80+
}, [deleteIndex, volumes, setVolumes]);
81+
82+
return (
83+
<>
84+
{volumes.length > 0 && (
85+
<Table variant={TableVariant.compact} aria-label="Volumes Table">
86+
<Thead>
87+
<Tr>
88+
<Th>PVC Name</Th>
89+
<Th>Mount Path</Th>
90+
<Th>Read-only Access</Th>
91+
<Th />
92+
</Tr>
93+
</Thead>
94+
<Tbody>
95+
{volumes.map((volume, index) => (
96+
<Tr key={index}>
97+
<Td>{volume.pvcName}</Td>
98+
<Td>{volume.mountPath}</Td>
99+
<Td>{volume.readOnly ? 'Enabled' : 'Disabled'}</Td>
100+
<Td isActionCell>
101+
<Dropdown
102+
toggle={(toggleRef) => (
103+
<MenuToggle
104+
ref={toggleRef}
105+
isExpanded={dropdownOpen === index}
106+
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
107+
variant="plain"
108+
aria-label="plain kebab"
109+
>
110+
<EllipsisVIcon />
111+
</MenuToggle>
112+
)}
113+
isOpen={dropdownOpen === index}
114+
onSelect={() => setDropdownOpen(null)}
115+
popperProps={{ position: 'right' }}
116+
>
117+
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
118+
<DropdownItem onClick={() => openDetachModal(index)}>Detach</DropdownItem>
119+
</Dropdown>
120+
</Td>
121+
</Tr>
122+
))}
123+
</Tbody>
124+
</Table>
125+
)}
126+
<Button
127+
variant="primary"
128+
onClick={() => setIsModalOpen(true)}
129+
style={{ marginTop: '1rem' }}
130+
className="pf-u-mt-md"
131+
>
132+
Create Volume
133+
</Button>
134+
135+
<Modal isOpen={isModalOpen} onClose={resetForm} variant={ModalVariant.small}>
136+
<ModalHeader
137+
title={editIndex !== null ? 'Edit Volume' : 'Create Volume'}
138+
description="Add a volume and optionally connect it with an existing workspace."
139+
/>
140+
<ModalBody>
141+
<Form>
142+
<FormGroup label="PVC Name" isRequired fieldId="pvc-name">
143+
<TextInput
144+
name="pvcName"
145+
isRequired
146+
type="text"
147+
value={formData.pvcName}
148+
onChange={(_, val) => setFormData({ ...formData, pvcName: val })}
149+
id="pvc-name"
150+
/>
151+
</FormGroup>
152+
<FormGroup label="Mount Path" isRequired fieldId="mount-path">
153+
<TextInput
154+
name="mountPath"
155+
isRequired
156+
type="text"
157+
value={formData.mountPath}
158+
onChange={(_, val) => setFormData({ ...formData, mountPath: val })}
159+
id="mount-path"
160+
/>
161+
</FormGroup>
162+
<FormGroup fieldId="readonly-access">
163+
<Switch
164+
id="readonly-access-switch"
165+
label="Enable read-only access"
166+
isChecked={formData.readOnly}
167+
onChange={() => setFormData({ ...formData, readOnly: !formData.readOnly })}
168+
/>
169+
</FormGroup>
170+
</Form>
171+
</ModalBody>
172+
<ModalFooter>
173+
<Button
174+
key="confirm"
175+
onClick={handleAddOrEdit}
176+
isDisabled={!formData.pvcName || !formData.mountPath}
177+
>
178+
{editIndex !== null ? 'Save' : 'Create'}
179+
</Button>
180+
<Button key="cancel" variant="link" onClick={resetForm}>
181+
Cancel
182+
</Button>
183+
</ModalFooter>
184+
</Modal>
185+
<Modal
186+
isOpen={isDeleteModalOpen}
187+
onClose={() => setIsDeleteModalOpen(false)}
188+
variant={ModalVariant.small}
189+
>
190+
<ModalHeader
191+
title="Detach Volume?"
192+
description="The volume and all of its resources will be detached from the workspace."
193+
/>
194+
<ModalFooter>
195+
<Button key="detach" variant="danger" onClick={handleDelete}>
196+
Detach
197+
</Button>
198+
<Button key="cancel" variant="link" onClick={() => setIsDeleteModalOpen(false)}>
199+
Cancel
200+
</Button>
201+
</ModalFooter>
202+
</Modal>
203+
</>
204+
);
205+
};
206+
207+
export default WorkspaceCreationPropertiesVolumesProps;

workspaces/frontend/src/shared/style/MUI-theme.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,8 +752,8 @@
752752
}
753753

754754
.mui-theme .pf-v6-c-modal-box {
755-
--pf-v6-c-modal-box--BorderRadius: 0;
756-
border: 2px solid var(--mui-palette-common-black);
755+
--pf-v6-c-modal-box--BorderRadius: var(--mui-shape-borderRadius);
756+
--pf-v6-c-modal-box--BoxShadow: var(--mui-shadows-24);
757757
}
758758

759759
.mui-theme .pf-v6-c-button.pf-m-plain {

0 commit comments

Comments
 (0)