Skip to content

Commit 06e1ca2

Browse files
committed
draft: attach modal
Signed-off-by: Charles Thao <[email protected]>
1 parent 291607e commit 06e1ca2

File tree

5 files changed

+397
-119
lines changed

5 files changed

+397
-119
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const isValidDefaultMode = (mode: string): boolean => {
2+
if (mode.length !== 3) {
3+
return false;
4+
}
5+
const permissions = ['0', '4', '5', '6', '7'];
6+
return Array.from(mode).every((char) => permissions.includes(char));
7+
};

workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx

Lines changed: 97 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,17 @@ import {
1212
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
1313
import {
1414
Modal,
15-
ModalBody,
1615
ModalFooter,
1716
ModalHeader,
1817
ModalVariant,
1918
} from '@patternfly/react-core/dist/esm/components/Modal';
20-
import { ValidatedOptions } from '@patternfly/react-core/helpers';
21-
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
2219
import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown';
2320
import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle';
24-
import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form';
25-
import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
2621
import { SecretsSecretListItem, WorkspacesPodSecretMount } from '~/generated/data-contracts';
2722
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
2823
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
24+
import { SecretsAttachModal } from './secrets/SecretsAttachModal';
25+
import { SecretsCreateModal } from './secrets/SecretsCreateModal';
2926

3027
interface WorkspaceFormPropertiesSecretsProps {
3128
secrets: WorkspacesPodSecretMount[];
@@ -38,20 +35,19 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
3835
secrets,
3936
setSecrets,
4037
}) => {
41-
const [isModalOpen, setIsModalOpen] = useState(false);
38+
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
39+
const [isAttachModalOpen, setIsAttachModalOpen] = useState(false);
4240
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
43-
const [formData, setFormData] = useState<WorkspacesPodSecretMount>({
44-
secretName: '',
45-
mountPath: '',
46-
defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8),
47-
});
41+
const [editingSecret, setEditingSecret] = useState<WorkspacesPodSecretMount | undefined>(
42+
undefined,
43+
);
4844
const [editIndex, setEditIndex] = useState<number | null>(null);
49-
const [defaultMode, setDefaultMode] = useState(DEFAULT_MODE_OCTAL);
5045
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
51-
const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
5246
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
5347
const [availableSecrets, setAvailableSecrets] = useState<SecretsSecretListItem[]>([]);
54-
const [attachedSecrets, setAttachedSecrets] = useState<SecretsSecretListItem[]>([]);
48+
const [attachedSecrets, setAttachedSecrets] = useState<WorkspacesPodSecretMount[]>([]);
49+
const [attachedMountPath, setAttachedMountPath] = useState('');
50+
const [attachedDefaultMode, setAttachedDefaultMode] = useState(DEFAULT_MODE_OCTAL);
5551

5652
const { api } = useNotebookAPI();
5753
const { selectedNamespace } = useNamespaceContext();
@@ -71,62 +67,86 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
7167

7268
const handleEdit = useCallback(
7369
(index: number) => {
74-
setFormData(secrets[index]);
75-
setDefaultMode(secrets[index].defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL);
70+
setEditingSecret(secrets[index]);
7671
setEditIndex(index);
77-
setIsModalOpen(true);
72+
setIsCreateModalOpen(true);
7873
},
7974
[secrets],
8075
);
8176

82-
const handleDefaultModeInput = useCallback(
83-
(val: string) => {
84-
if (val.length <= 3) {
85-
// 0 no permissions, 4 read only, 5 read + execute, 6 read + write, 7 all permissions
86-
setDefaultMode(val);
87-
const permissions = ['0', '4', '5', '6', '7'];
88-
const isValid = Array.from(val).every((char) => permissions.includes(char));
89-
if (val.length < 3 || !isValid) {
90-
setIsDefaultModeValid(false);
91-
} else {
92-
setIsDefaultModeValid(true);
93-
}
94-
const decimalVal = parseInt(val, 8);
95-
setFormData({ ...formData, defaultMode: decimalVal });
77+
const handleAttachSecrets = useCallback(
78+
(newSecrets: SecretsSecretListItem[], mountPath: string, mode: number) => {
79+
const newAttachedSecrets = newSecrets.map((secret) => ({
80+
secretName: secret.name,
81+
mountPath,
82+
defaultMode: mode,
83+
}));
84+
const oldAttachedNames = new Set(attachedSecrets.map((s) => s.secretName));
85+
const secretsWithoutOldAttached = secrets.filter((s) => !oldAttachedNames.has(s.secretName));
86+
const manualSecretNames = new Set(secretsWithoutOldAttached.map((s) => s.secretName));
87+
const filteredNewAttached = newAttachedSecrets.filter(
88+
(s) => !manualSecretNames.has(s.secretName),
89+
);
90+
91+
// Update both states
92+
setAttachedSecrets(filteredNewAttached);
93+
setSecrets([...secretsWithoutOldAttached, ...filteredNewAttached]);
94+
setAttachedMountPath(mountPath);
95+
setAttachedDefaultMode(mode.toString(8));
96+
setIsAttachModalOpen(false);
97+
},
98+
[attachedSecrets, secrets, setSecrets],
99+
);
100+
101+
const handleCreateOrEditSubmit = useCallback(
102+
(secret: WorkspacesPodSecretMount) => {
103+
if (editIndex !== null) {
104+
const updated = [...secrets];
105+
updated[editIndex] = secret;
106+
setSecrets(updated);
107+
} else {
108+
setSecrets([...secrets, secret]);
96109
}
110+
setEditingSecret(undefined);
111+
setEditIndex(null);
112+
setIsCreateModalOpen(false);
97113
},
98-
[setFormData, setIsDefaultModeValid, setDefaultMode, formData],
114+
[editIndex, secrets, setSecrets],
99115
);
100116

101-
const clearForm = useCallback(() => {
102-
setFormData({ secretName: '', mountPath: '', defaultMode: 420 });
117+
const handleCreateModalClose = useCallback(() => {
118+
setEditingSecret(undefined);
103119
setEditIndex(null);
104-
setIsModalOpen(false);
105-
setIsDefaultModeValid(true);
120+
setIsCreateModalOpen(false);
106121
}, []);
107122

108-
const handleAddOrEditSubmit = useCallback(() => {
109-
if (!formData.secretName || !formData.mountPath) {
110-
return;
111-
}
112-
if (editIndex !== null) {
113-
const updated = [...secrets];
114-
updated[editIndex] = formData;
115-
setSecrets(updated);
116-
} else {
117-
setSecrets([...secrets, formData]);
118-
}
119-
clearForm();
120-
}, [clearForm, editIndex, formData, secrets, setSecrets]);
123+
const isAttachedSecret = useCallback(
124+
(secretName: string) => attachedSecrets.some((s) => s.secretName === secretName),
125+
[attachedSecrets],
126+
);
121127

122128
const handleDelete = useCallback(() => {
123129
if (deleteIndex === null) {
124130
return;
125131
}
132+
const secretToDelete = secrets[deleteIndex];
126133
setSecrets(secrets.filter((_, i) => i !== deleteIndex));
134+
135+
// If it's an attached secret, also remove from attachedSecrets
136+
if (isAttachedSecret(secretToDelete.secretName)) {
137+
const updatedAttachedSecrets = attachedSecrets.filter(
138+
(s) => s.secretName !== secretToDelete.secretName,
139+
);
140+
setAttachedSecrets(updatedAttachedSecrets);
141+
if (updatedAttachedSecrets.length === 0) {
142+
setAttachedMountPath('');
143+
setAttachedDefaultMode(DEFAULT_MODE_OCTAL);
144+
}
145+
}
146+
127147
setDeleteIndex(null);
128148
setIsDeleteModalOpen(false);
129-
}, [deleteIndex, secrets, setSecrets]);
149+
}, [deleteIndex, secrets, setSecrets, attachedSecrets, isAttachedSecret]);
130150

131151
return (
132152
<>
@@ -163,7 +183,9 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
163183
onSelect={() => setDropdownOpen(null)}
164184
popperProps={{ position: 'right' }}
165185
>
166-
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
186+
{!isAttachedSecret(secret.secretName) && (
187+
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
188+
)}
167189
<DropdownItem onClick={() => openDeleteModal(index)}>Remove</DropdownItem>
168190
</Dropdown>
169191
</Td>
@@ -173,78 +195,34 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
173195
</Table>
174196
)}
175197
<Button
176-
variant="primary"
177-
onClick={() => setIsModalOpen(true)}
198+
variant="secondary"
199+
onClick={() => setIsAttachModalOpen(true)}
200+
style={{ marginTop: '1rem', marginRight: '1rem', width: 'fit-content' }}
201+
>
202+
Attach Existing Secrets
203+
</Button>
204+
<Button
205+
variant="secondary"
206+
onClick={() => setIsCreateModalOpen(true)}
178207
style={{ marginTop: '1rem', width: 'fit-content' }}
179208
>
180209
Create Secret
181210
</Button>
182-
<Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
183-
<ModalHeader
184-
title={editIndex === null ? 'Create Secret' : 'Edit Secret'}
185-
labelId="secret-modal-title"
186-
description={
187-
editIndex === null
188-
? 'Add a secret to securely use API keys, tokens, or other credentials in your workspace.'
189-
: ''
190-
}
191-
/>
192-
<ModalBody id="secret-modal-box-body">
193-
<Form onSubmit={handleAddOrEditSubmit}>
194-
<FormGroup label="Secret Name" isRequired fieldId="secret-name">
195-
<TextInput
196-
name="secretName"
197-
isRequired
198-
type="text"
199-
value={formData.secretName}
200-
onChange={(_, val) => setFormData({ ...formData, secretName: val })}
201-
id="secret-name"
202-
/>
203-
</FormGroup>
204-
<FormGroup label="Mount Path" isRequired fieldId="mount-path">
205-
<TextInput
206-
name="mountPath"
207-
isRequired
208-
type="text"
209-
value={formData.mountPath}
210-
onChange={(_, val) => setFormData({ ...formData, mountPath: val })}
211-
id="mount-path"
212-
/>
213-
</FormGroup>
214-
<FormGroup label="Default Mode" isRequired fieldId="default-mode">
215-
<TextInput
216-
name="defaultMode"
217-
isRequired
218-
type="text"
219-
value={defaultMode}
220-
validated={!isDefaultModeValid ? ValidatedOptions.error : undefined}
221-
onChange={(_, val) => handleDefaultModeInput(val)}
222-
id="default-mode"
223-
/>
224-
{!isDefaultModeValid && (
225-
<HelperText>
226-
<HelperTextItem variant="error">
227-
Must be a valid UNIX file system permission value (i.e. 644)
228-
</HelperTextItem>
229-
</HelperText>
230-
)}
231-
</FormGroup>
232-
</Form>
233-
</ModalBody>
234-
<ModalFooter>
235-
<Button
236-
key="confirm"
237-
variant="primary"
238-
onClick={handleAddOrEditSubmit}
239-
isDisabled={!isDefaultModeValid}
240-
>
241-
{editIndex !== null ? 'Save' : 'Create'}
242-
</Button>
243-
<Button key="cancel" variant="link" onClick={clearForm}>
244-
Cancel
245-
</Button>
246-
</ModalFooter>
247-
</Modal>
211+
<SecretsAttachModal
212+
availableSecrets={availableSecrets}
213+
isOpen={isAttachModalOpen}
214+
setIsOpen={setIsAttachModalOpen}
215+
selectedSecrets={attachedSecrets.map((secret) => secret.secretName)}
216+
onClose={handleAttachSecrets}
217+
initialMountPath={attachedMountPath}
218+
initialDefaultMode={attachedDefaultMode}
219+
/>
220+
<SecretsCreateModal
221+
isOpen={isCreateModalOpen}
222+
setIsOpen={handleCreateModalClose}
223+
onSubmit={handleCreateOrEditSubmit}
224+
editSecret={editingSecret}
225+
/>
248226
<Modal
249227
isOpen={isDeleteModalOpen}
250228
onClose={() => setIsDeleteModalOpen(false)}

0 commit comments

Comments
 (0)