Skip to content

Commit 9c0a3ce

Browse files
committed
feat(ws): Add secrets to workspace creation properties form
1 parent ecee78a commit 9c0a3ce

File tree

4 files changed

+289
-2
lines changed

4 files changed

+289
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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+
var isValid = Array.from(val).every((char) => permissions.includes(char));
67+
if (val.length < 3 || !isValid) setIsDefaultModeValid(false);
68+
else setIsDefaultModeValid(true);
69+
var decimalVal = parseInt(val, 8);
70+
setFormData({ ...formData, defaultMode: decimalVal });
71+
}
72+
},
73+
[setFormData, setIsDefaultModeValid, setDefaultMode, formData],
74+
);
75+
76+
const clearForm = useCallback(() => {
77+
setFormData({ secretName: '', mountPath: '', defaultMode: 420 });
78+
setEditIndex(null);
79+
setIsModalOpen(false);
80+
setIsDefaultModeValid(true);
81+
}, []);
82+
83+
const handleAddOrEditSubmit = useCallback(() => {
84+
setSecrets((prev) => {
85+
if (editIndex === null) return [...prev, formData];
86+
else {
87+
const updated = prev;
88+
updated[editIndex] = formData;
89+
return updated;
90+
}
91+
});
92+
clearForm();
93+
}, [editIndex, setSecrets, clearForm, formData]);
94+
95+
const handleDelete = useCallback(() => {
96+
console.log(deleteIndex)
97+
if (deleteIndex !== null) {
98+
setSecrets((prev) => {
99+
prev.splice(deleteIndex, 1);
100+
return prev;
101+
});
102+
setDeleteIndex(null);
103+
setIsDeleteModalOpen(false);
104+
}
105+
}, [deleteIndex]);
106+
107+
return (
108+
<>
109+
{secrets.length > 0 && (
110+
<Table variant={TableVariant.compact} aria-label="Secrets Table">
111+
<Thead>
112+
<Tr>
113+
<Th>Secret Name</Th>
114+
<Th>Mount Path</Th>
115+
<Th>Default Mode</Th>
116+
<Th />
117+
</Tr>
118+
</Thead>
119+
<Tbody>
120+
{secrets.map((secret, index) => (
121+
<Tr key={index}>
122+
<Td>{secret.secretName}</Td>
123+
<Td>{secret.mountPath}</Td>
124+
<Td>{secret.defaultMode.toString(8)}</Td>
125+
<Td isActionCell>
126+
<Dropdown
127+
toggle={(toggleRef) => (
128+
<MenuToggle
129+
ref={toggleRef}
130+
isExpanded={dropdownOpen === index}
131+
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
132+
variant="plain"
133+
aria-label="plain kebab"
134+
>
135+
<EllipsisVIcon />
136+
</MenuToggle>
137+
)}
138+
isOpen={dropdownOpen === index}
139+
onSelect={() => setDropdownOpen(null)}
140+
popperProps={{ position: 'right' }}
141+
>
142+
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
143+
<DropdownItem onClick={() => openDeleteModal(index)}>Remove</DropdownItem>
144+
</Dropdown>
145+
</Td>
146+
</Tr>
147+
))}
148+
</Tbody>
149+
</Table>
150+
)}
151+
<Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
152+
Create Secrets
153+
</Button>
154+
<Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
155+
<ModalHeader
156+
title={editIndex === null ? 'Create Secret' : 'Edit Secret'}
157+
labelId="secret-modal-title"
158+
/>
159+
<ModalBody id="secret-modal-box-body">
160+
<Form onSubmit={handleAddOrEditSubmit}>
161+
<FormGroup label="Secret Name" isRequired fieldId="secret-name">
162+
<TextInput
163+
name="secretName"
164+
isRequired
165+
type="text"
166+
value={formData.secretName}
167+
onChange={(_, val) => setFormData({ ...formData, secretName: val })}
168+
id="secret-name"
169+
/>
170+
</FormGroup>
171+
<FormGroup label="Mount Path" isRequired fieldId="mount-path">
172+
<TextInput
173+
name="mountPath"
174+
isRequired
175+
type="text"
176+
value={formData.mountPath}
177+
onChange={(_, val) => setFormData({ ...formData, mountPath: val })}
178+
id="mount-path"
179+
/>
180+
</FormGroup>
181+
<FormGroup label="Default Mode" isRequired fieldId="default-mode">
182+
<TextInput
183+
name="defaultMode"
184+
isRequired
185+
type="text"
186+
value={defaultMode}
187+
validated={!isDefaultModeValid ? ValidatedOptions.error : undefined}
188+
onChange={(_, val) => handleDefaultModeInput(val)}
189+
id="default-mode"
190+
/>
191+
{!isDefaultModeValid && (
192+
<HelperText>
193+
<HelperTextItem variant="error">
194+
Must be a valid UNIX file system permission value (i.e. 644)
195+
</HelperTextItem>
196+
</HelperText>
197+
)}
198+
</FormGroup>
199+
</Form>
200+
</ModalBody>
201+
<ModalFooter>
202+
<Button
203+
key="confirm"
204+
variant="primary"
205+
onClick={handleAddOrEditSubmit}
206+
isDisabled={!isDefaultModeValid}
207+
>
208+
{editIndex !== null ? 'Save' : 'Create'}
209+
</Button>
210+
<Button key="cancel" variant="link" onClick={clearForm}>
211+
Cancel
212+
</Button>
213+
</ModalFooter>
214+
</Modal>
215+
<Modal
216+
isOpen={isDeleteModalOpen}
217+
onClose={() => setIsDeleteModalOpen(false)}
218+
variant={ModalVariant.small}
219+
>
220+
<ModalHeader
221+
title="Remove Secret?"
222+
description="The secret will be removed from the workspace."
223+
/>
224+
<ModalFooter>
225+
<Button key="remove" variant="danger" onClick={handleDelete}>
226+
Remove
227+
</Button>
228+
<Button key="cancel" variant="link" onClick={() => setIsDeleteModalOpen(false)}>
229+
Cancel
230+
</Button>
231+
</ModalFooter>
232+
</Modal>
233+
</>
234+
);
235+
};
236+
237+
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)