Skip to content

Commit 24c4046

Browse files
committed
feat(ws): Add secrets to workspace creation properties form
Signed-off-by: Charles Thao <[email protected]>
1 parent ecee78a commit 24c4046

File tree

4 files changed

+291
-2
lines changed

4 files changed

+291
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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+
Dropdown,
10+
DropdownItem,
11+
MenuToggle,
12+
ModalBody,
13+
ModalFooter,
14+
Form,
15+
FormGroup,
16+
ModalHeader,
17+
ValidatedOptions,
18+
HelperText,
19+
HelperTextItem,
20+
} from '@patternfly/react-core';
21+
import { WorkSpaceSecret } from '~/shared/types';
22+
23+
interface WorkspaceCreationSecretsProps {
24+
secrets: WorkSpaceSecret[];
25+
setSecrets: React.Dispatch<React.SetStateAction<WorkSpaceSecret[]>>;
26+
}
27+
28+
export const WorkspaceCreationPropertiesSecrets: React.FC<WorkspaceCreationSecretsProps> = ({
29+
secrets,
30+
setSecrets,
31+
}) => {
32+
const [isModalOpen, setIsModalOpen] = useState(false);
33+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
34+
const [formData, setFormData] = useState<WorkSpaceSecret>({
35+
secretName: '',
36+
mountPath: '',
37+
defaultMode: 420,
38+
});
39+
const [editIndex, setEditIndex] = useState<number | null>(null);
40+
const [defaultMode, setDefaultMode] = useState('644');
41+
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
42+
const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
43+
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
44+
45+
const openDeleteModal = useCallback((i: number) => {
46+
setIsDeleteModalOpen(true);
47+
setDeleteIndex(i);
48+
}, []);
49+
50+
const handleEdit = useCallback(
51+
(index: number) => {
52+
setFormData(secrets[index]);
53+
setDefaultMode(secrets[index].defaultMode.toString(8));
54+
setEditIndex(index);
55+
setIsModalOpen(true);
56+
},
57+
[secrets],
58+
);
59+
60+
const handleDefaultModeInput = useCallback(
61+
(val: string) => {
62+
if (val.length <= 3) {
63+
// 0 no permissions, 4 read only, 5 read + execute, 6 read + write, 7 all permissions
64+
setDefaultMode(val);
65+
const permissions = ['0', '4', '5', '6', '7'];
66+
const isValid = Array.from(val).every((char) => permissions.includes(char));
67+
if (val.length < 3 || !isValid) {
68+
setIsDefaultModeValid(false);
69+
} else {
70+
setIsDefaultModeValid(true);
71+
}
72+
const decimalVal = parseInt(val, 8);
73+
setFormData({ ...formData, defaultMode: decimalVal });
74+
}
75+
},
76+
[setFormData, setIsDefaultModeValid, setDefaultMode, formData],
77+
);
78+
79+
const clearForm = useCallback(() => {
80+
setFormData({ secretName: '', mountPath: '', defaultMode: 420 });
81+
setEditIndex(null);
82+
setIsModalOpen(false);
83+
setIsDefaultModeValid(true);
84+
}, []);
85+
86+
const handleAddOrEditSubmit = useCallback(() => {
87+
setSecrets((prev) => {
88+
if (editIndex === null) {
89+
return [...prev, formData];
90+
}
91+
const updated = prev;
92+
updated[editIndex] = formData;
93+
return updated;
94+
});
95+
clearForm();
96+
}, [editIndex, setSecrets, clearForm, formData]);
97+
98+
const handleDelete = useCallback(() => {
99+
if (deleteIndex !== null) {
100+
setSecrets((prev) => {
101+
prev.splice(deleteIndex, 1);
102+
return prev;
103+
});
104+
setDeleteIndex(null);
105+
setIsDeleteModalOpen(false);
106+
}
107+
}, [setSecrets, deleteIndex]);
108+
109+
return (
110+
<>
111+
{secrets.length > 0 && (
112+
<Table variant={TableVariant.compact} aria-label="Secrets Table">
113+
<Thead>
114+
<Tr>
115+
<Th>Secret Name</Th>
116+
<Th>Mount Path</Th>
117+
<Th>Default Mode</Th>
118+
<Th />
119+
</Tr>
120+
</Thead>
121+
<Tbody>
122+
{secrets.map((secret, index) => (
123+
<Tr key={index}>
124+
<Td>{secret.secretName}</Td>
125+
<Td>{secret.mountPath}</Td>
126+
<Td>{secret.defaultMode.toString(8)}</Td>
127+
<Td isActionCell>
128+
<Dropdown
129+
toggle={(toggleRef) => (
130+
<MenuToggle
131+
ref={toggleRef}
132+
isExpanded={dropdownOpen === index}
133+
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
134+
variant="plain"
135+
aria-label="plain kebab"
136+
>
137+
<EllipsisVIcon />
138+
</MenuToggle>
139+
)}
140+
isOpen={dropdownOpen === index}
141+
onSelect={() => setDropdownOpen(null)}
142+
popperProps={{ position: 'right' }}
143+
>
144+
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
145+
<DropdownItem onClick={() => openDeleteModal(index)}>Remove</DropdownItem>
146+
</Dropdown>
147+
</Td>
148+
</Tr>
149+
))}
150+
</Tbody>
151+
</Table>
152+
)}
153+
<Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
154+
Create Secrets
155+
</Button>
156+
<Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
157+
<ModalHeader
158+
title={editIndex === null ? 'Create Secret' : 'Edit Secret'}
159+
labelId="secret-modal-title"
160+
/>
161+
<ModalBody id="secret-modal-box-body">
162+
<Form onSubmit={handleAddOrEditSubmit}>
163+
<FormGroup label="Secret Name" isRequired fieldId="secret-name">
164+
<TextInput
165+
name="secretName"
166+
isRequired
167+
type="text"
168+
value={formData.secretName}
169+
onChange={(_, val) => setFormData({ ...formData, secretName: val })}
170+
id="secret-name"
171+
/>
172+
</FormGroup>
173+
<FormGroup label="Mount Path" isRequired fieldId="mount-path">
174+
<TextInput
175+
name="mountPath"
176+
isRequired
177+
type="text"
178+
value={formData.mountPath}
179+
onChange={(_, val) => setFormData({ ...formData, mountPath: val })}
180+
id="mount-path"
181+
/>
182+
</FormGroup>
183+
<FormGroup label="Default Mode" isRequired fieldId="default-mode">
184+
<TextInput
185+
name="defaultMode"
186+
isRequired
187+
type="text"
188+
value={defaultMode}
189+
validated={!isDefaultModeValid ? ValidatedOptions.error : undefined}
190+
onChange={(_, val) => handleDefaultModeInput(val)}
191+
id="default-mode"
192+
/>
193+
{!isDefaultModeValid && (
194+
<HelperText>
195+
<HelperTextItem variant="error">
196+
Must be a valid UNIX file system permission value (i.e. 644)
197+
</HelperTextItem>
198+
</HelperText>
199+
)}
200+
</FormGroup>
201+
</Form>
202+
</ModalBody>
203+
<ModalFooter>
204+
<Button
205+
key="confirm"
206+
variant="primary"
207+
onClick={handleAddOrEditSubmit}
208+
isDisabled={!isDefaultModeValid}
209+
>
210+
{editIndex !== null ? 'Save' : 'Create'}
211+
</Button>
212+
<Button key="cancel" variant="link" onClick={clearForm}>
213+
Cancel
214+
</Button>
215+
</ModalFooter>
216+
</Modal>
217+
<Modal
218+
isOpen={isDeleteModalOpen}
219+
onClose={() => setIsDeleteModalOpen(false)}
220+
variant={ModalVariant.small}
221+
>
222+
<ModalHeader
223+
title="Remove Secret?"
224+
description="The secret will be removed from the workspace."
225+
/>
226+
<ModalFooter>
227+
<Button key="remove" variant="danger" onClick={handleDelete}>
228+
Remove
229+
</Button>
230+
<Button key="cancel" variant="link" onClick={() => setIsDeleteModalOpen(false)}>
231+
Cancel
232+
</Button>
233+
</ModalFooter>
234+
</Modal>
235+
</>
236+
);
237+
};
238+
239+
export default WorkspaceCreationPropertiesSecrets;

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

+31-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
} from '@patternfly/react-core';
1414
import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails';
1515
import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumesProps';
16-
import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume } from '~/shared/types';
16+
import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume, WorkSpaceSecret } from '~/shared/types';
17+
import WorkspaceCreationPropertiesSecrets from './WorkspaceCreationPropertiesSecrets';
1718

1819
interface WorkspaceCreationImageSelectionProps {
1920
selectedImage: WorkspaceImage | undefined;
@@ -25,9 +26,13 @@ const WorkspaceCreationPropertiesSelection: React.FunctionComponent<
2526
const [workspaceName, setWorkspaceName] = useState('');
2627
const [deferUpdates, setDeferUpdates] = useState(false);
2728
const [homeDirectory, setHomeDirectory] = useState('');
28-
const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [] });
29+
const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [], secrets: [] });
2930
const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]);
31+
const [secrets, setSecrets] = useState<WorkSpaceSecret[]>(
32+
volumes.secrets.length ? volumes.secrets : [],
33+
);
3034
const [isVolumesExpanded, setIsVolumesExpanded] = useState(false);
35+
const [isSecretsExpanded, setIsSecretsExpanded] = useState(false);
3136

3237
React.useEffect(() => {
3338
setVolumes((prev) => ({
@@ -115,6 +120,30 @@ const WorkspaceCreationPropertiesSelection: React.FunctionComponent<
115120
</div>
116121
</div>
117122
)}
123+
<div className="pf-u-mb-0">
124+
<ExpandableSection
125+
toggleText="Secrets"
126+
onToggle={() => setIsSecretsExpanded((prev) => !prev)}
127+
isExpanded={isSecretsExpanded}
128+
isIndented
129+
>
130+
{isSecretsExpanded && (
131+
<FormGroup fieldId="secrets-table" style={{ marginTop: '1rem' }}>
132+
<WorkspaceCreationPropertiesSecrets
133+
secrets={secrets}
134+
setSecrets={setSecrets}
135+
/>
136+
</FormGroup>
137+
)}
138+
</ExpandableSection>
139+
{!isSecretsExpanded && (
140+
<div style={{ paddingLeft: '36px' }}>
141+
<div className="pf-u-font-size-sm">
142+
<strong>{secrets.length} added</strong>
143+
</div>
144+
</div>
145+
)}
146+
</div>
118147
</Form>
119148
</div>
120149
</SplitItem>

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

+14
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ export const Workspaces: React.FunctionComponent = () => {
8989
readOnly: false,
9090
},
9191
],
92+
secrets: [
93+
{
94+
secretName: 'Secret-2',
95+
mountPath: '/data',
96+
defaultMode: 420,
97+
},
98+
],
9299
},
93100
endpoints: [
94101
{
@@ -144,6 +151,13 @@ export const Workspaces: React.FunctionComponent = () => {
144151
readOnly: false,
145152
},
146153
],
154+
secrets: [
155+
{
156+
secretName: 'workspace-secret',
157+
mountPath: '/secrets/my-secret',
158+
defaultMode: 420,
159+
},
160+
],
147161
},
148162
endpoints: [
149163
{

workspaces/frontend/src/shared/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export interface WorkspaceVolume {
2929
export interface WorkspaceVolumes {
3030
home: string;
3131
data: WorkspaceVolume[];
32+
secrets: WorkSpaceSecret[];
33+
}
34+
35+
export interface WorkSpaceSecret {
36+
defaultMode: number;
37+
secretName: string;
38+
mountPath: string;
3239
}
3340

3441
export interface WorkspaceProperties {

0 commit comments

Comments
 (0)