Skip to content

Commit 4f72bbe

Browse files
committed
draft: attach modal
Signed-off-by: Charles Thao <cthao@redhat.com>
1 parent 2004531 commit 4f72bbe

File tree

5 files changed

+406
-121
lines changed

5 files changed

+406
-121
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: 107 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22
import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
33
import {
44
Table,
@@ -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,95 @@ 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+
// Create the new attached secrets list
80+
const newAttachedSecrets = newSecrets.map((secret) => ({
81+
secretName: secret.name,
82+
mountPath,
83+
defaultMode: mode,
84+
}));
85+
86+
// Get the secret names that were previously attached
87+
const oldAttachedNames = new Set(attachedSecrets.map((s) => s.secretName));
88+
89+
// Remove old attached secrets from the main secrets array
90+
const secretsWithoutOldAttached = secrets.filter((s) => !oldAttachedNames.has(s.secretName));
91+
92+
// Filter out any new secrets that already exist in the manually created secrets
93+
const manualSecretNames = new Set(secretsWithoutOldAttached.map((s) => s.secretName));
94+
const filteredNewAttached = newAttachedSecrets.filter(
95+
(s) => !manualSecretNames.has(s.secretName),
96+
);
97+
98+
// Update both states
99+
setAttachedSecrets(filteredNewAttached);
100+
setSecrets([...secretsWithoutOldAttached, ...filteredNewAttached]);
101+
setAttachedMountPath(mountPath);
102+
setAttachedDefaultMode(mode.toString(8));
103+
setIsAttachModalOpen(false);
104+
},
105+
[attachedSecrets, secrets, setSecrets],
106+
);
107+
108+
const handleCreateOrEditSubmit = useCallback(
109+
(secret: WorkspacesPodSecretMount) => {
110+
if (editIndex !== null) {
111+
const updated = [...secrets];
112+
updated[editIndex] = secret;
113+
setSecrets(updated);
114+
} else {
115+
setSecrets([...secrets, secret]);
96116
}
117+
setEditingSecret(undefined);
118+
setEditIndex(null);
119+
setIsCreateModalOpen(false);
97120
},
98-
[setFormData, setIsDefaultModeValid, setDefaultMode, formData],
121+
[editIndex, secrets, setSecrets],
99122
);
100123

101-
const clearForm = useCallback(() => {
102-
setFormData({ secretName: '', mountPath: '', defaultMode: 420 });
124+
const handleCreateModalClose = useCallback(() => {
125+
setEditingSecret(undefined);
103126
setEditIndex(null);
104-
setIsModalOpen(false);
105-
setIsDefaultModeValid(true);
127+
setIsCreateModalOpen(false);
106128
}, []);
107129

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]);
130+
const isAttachedSecret = useCallback(
131+
(secretName: string) => attachedSecrets.some((s) => s.secretName === secretName),
132+
[attachedSecrets],
133+
);
121134

122135
const handleDelete = useCallback(() => {
123136
if (deleteIndex === null) {
124137
return;
125138
}
139+
const secretToDelete = secrets[deleteIndex];
140+
141+
// Remove from secrets array
126142
setSecrets(secrets.filter((_, i) => i !== deleteIndex));
143+
144+
// If it's an attached secret, also remove from attachedSecrets
145+
if (isAttachedSecret(secretToDelete.secretName)) {
146+
const updatedAttachedSecrets = attachedSecrets.filter(
147+
(s) => s.secretName !== secretToDelete.secretName,
148+
);
149+
setAttachedSecrets(updatedAttachedSecrets);
150+
if (updatedAttachedSecrets.length === 0) {
151+
setAttachedMountPath('');
152+
setAttachedDefaultMode(DEFAULT_MODE_OCTAL);
153+
}
154+
}
155+
127156
setDeleteIndex(null);
128157
setIsDeleteModalOpen(false);
129-
}, [deleteIndex, secrets, setSecrets]);
158+
}, [deleteIndex, secrets, setSecrets, attachedSecrets, isAttachedSecret]);
130159

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

0 commit comments

Comments
 (0)