Skip to content

Commit 83418b2

Browse files
committed
feat: implemet create new secret modal
Signed-off-by: Jenny <[email protected]>
1 parent 432f941 commit 83418b2

File tree

6 files changed

+453
-162
lines changed

6 files changed

+453
-162
lines changed

workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { css } from '@patternfly/react-styles';
1212
import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
1313
import { TrashAltIcon } from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon';
1414
import { WorkspacekindsOptionLabel } from '~/generated/data-contracts';
15+
import ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper';
1516

1617
interface EditableRowInterface {
1718
data: WorkspacekindsOptionLabel;
@@ -33,14 +34,16 @@ const EditableRow: React.FC<EditableRowInterface> = ({
3334
return (
3435
<Tr className={css(inlineEditStyles.inlineEdit, inlineEditStyles.modifiers.inlineEditable)}>
3536
<Td>
36-
<TextInput
37-
aria-label={`${columnNames.key} ${ariaLabel}`}
38-
id={`${columnNames.key} ${ariaLabel} key`}
39-
ref={inputRef}
40-
value={data.key}
41-
onChange={(e) => saveChanges({ ...data, key: (e.target as HTMLInputElement).value })}
42-
placeholder="Enter key"
43-
/>
37+
<ThemeAwareFormGroupWrapper isRequired fieldId="key">
38+
<TextInput
39+
aria-label={`${columnNames.key} ${ariaLabel}`}
40+
id={`${columnNames.key} ${ariaLabel} key`}
41+
ref={inputRef}
42+
value={data.key}
43+
onChange={(e) => saveChanges({ ...data, key: (e.target as HTMLInputElement).value })}
44+
placeholder="Enter key"
45+
/>
46+
</ThemeAwareFormGroupWrapper>
4447
</Td>
4548
<Td>
4649
<TextInput

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

Lines changed: 29 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,36 @@ 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 } from '@patternfly/react-core/dist/esm/components/Form';
25-
import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText';
26-
import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
27-
import ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper';
2821
import { SecretsSecretListItem, WorkspacesPodSecretMount } from '~/generated/data-contracts';
2922
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
3023
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
24+
import { SecretsApiCreateModal } from './secrets/SecretsApiCreateModal';
3125

3226
interface WorkspaceFormPropertiesSecretsProps {
3327
secrets: WorkspacesPodSecretMount[];
3428
setSecrets: (secrets: WorkspacesPodSecretMount[]) => void;
3529
}
3630

37-
const DEFAULT_MODE_OCTAL = (420).toString(8);
38-
3931
export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSecretsProps> = ({
4032
secrets,
4133
setSecrets,
4234
}) => {
43-
const [isModalOpen, setIsModalOpen] = useState(false);
35+
const { api } = useNotebookAPI();
36+
const { selectedNamespace } = useNamespaceContext();
37+
38+
const [isApiCreateModalOpen, setIsApiCreateModalOpen] = useState(false);
4439
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
45-
const [formData, setFormData] = useState<WorkspacesPodSecretMount>({
46-
secretName: '',
47-
mountPath: '',
48-
defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8),
49-
});
50-
const [editIndex, setEditIndex] = useState<number | null>(null);
51-
const [defaultMode, setDefaultMode] = useState(DEFAULT_MODE_OCTAL);
5240
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
53-
const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
5441
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
42+
// Keep baseline secrets fetching from PR #698 for future attach functionality
5543
const [, setAvailableSecrets] = useState<SecretsSecretListItem[]>([]);
5644

57-
const { api } = useNotebookAPI();
58-
const { selectedNamespace } = useNamespaceContext();
59-
6045
useEffect(() => {
6146
const fetchSecrets = async () => {
6247
const secretsResponse = await api.secrets.listSecrets(selectedNamespace);
@@ -70,56 +55,6 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
7055
setDeleteIndex(i);
7156
}, []);
7257

73-
const handleEdit = useCallback(
74-
(index: number) => {
75-
setFormData(secrets[index]);
76-
setDefaultMode(secrets[index].defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL);
77-
setEditIndex(index);
78-
setIsModalOpen(true);
79-
},
80-
[secrets],
81-
);
82-
83-
const handleDefaultModeInput = useCallback(
84-
(val: string) => {
85-
if (val.length <= 3) {
86-
// 0 no permissions, 4 read only, 5 read + execute, 6 read + write, 7 all permissions
87-
setDefaultMode(val);
88-
const permissions = ['0', '4', '5', '6', '7'];
89-
const isValid = Array.from(val).every((char) => permissions.includes(char));
90-
if (val.length < 3 || !isValid) {
91-
setIsDefaultModeValid(false);
92-
} else {
93-
setIsDefaultModeValid(true);
94-
}
95-
const decimalVal = parseInt(val, 8);
96-
setFormData({ ...formData, defaultMode: decimalVal });
97-
}
98-
},
99-
[setFormData, setIsDefaultModeValid, setDefaultMode, formData],
100-
);
101-
102-
const clearForm = useCallback(() => {
103-
setFormData({ secretName: '', mountPath: '', defaultMode: 420 });
104-
setEditIndex(null);
105-
setIsModalOpen(false);
106-
setIsDefaultModeValid(true);
107-
}, []);
108-
109-
const handleAddOrEditSubmit = useCallback(() => {
110-
if (!formData.secretName || !formData.mountPath) {
111-
return;
112-
}
113-
if (editIndex !== null) {
114-
const updated = [...secrets];
115-
updated[editIndex] = formData;
116-
setSecrets(updated);
117-
} else {
118-
setSecrets([...secrets, formData]);
119-
}
120-
clearForm();
121-
}, [clearForm, editIndex, formData, secrets, setSecrets]);
122-
12358
const handleDelete = useCallback(() => {
12459
if (deleteIndex === null) {
12560
return;
@@ -146,7 +81,7 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
14681
<Tr key={index}>
14782
<Td>{secret.secretName}</Td>
14883
<Td>{secret.mountPath}</Td>
149-
<Td>{secret.defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL}</Td>
84+
<Td>{secret.defaultMode?.toString(8) ?? '644'}</Td>
15085
<Td isActionCell>
15186
<Dropdown
15287
toggle={(toggleRef) => (
@@ -164,7 +99,6 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
16499
onSelect={() => setDropdownOpen(null)}
165100
popperProps={{ position: 'right' }}
166101
>
167-
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
168102
<DropdownItem onClick={() => openDeleteModal(index)}>Remove</DropdownItem>
169103
</Dropdown>
170104
</Td>
@@ -173,86 +107,28 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
173107
</Tbody>
174108
</Table>
175109
)}
176-
<Button
177-
variant="primary"
178-
icon={<PlusCircleIcon />}
179-
onClick={() => setIsModalOpen(true)}
180-
style={{ marginTop: '1rem', width: 'fit-content' }}
181-
>
182-
Create Secret
183-
</Button>
184-
<Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
185-
<ModalHeader
186-
title={editIndex === null ? 'Create Secret' : 'Edit Secret'}
187-
labelId="secret-modal-title"
188-
description={
189-
editIndex === null
190-
? 'Add a secret to securely use API keys, tokens, or other credentials in your workspace.'
191-
: ''
192-
}
193-
/>
194-
<ModalBody id="secret-modal-box-body">
195-
<Form onSubmit={handleAddOrEditSubmit}>
196-
<ThemeAwareFormGroupWrapper label="Secret Name" isRequired fieldId="secret-name">
197-
<TextInput
198-
name="secretName"
199-
isRequired
200-
type="text"
201-
value={formData.secretName}
202-
onChange={(_, val) => setFormData({ ...formData, secretName: val })}
203-
id="secret-name"
204-
/>
205-
</ThemeAwareFormGroupWrapper>
206-
<ThemeAwareFormGroupWrapper label="Mount Path" isRequired fieldId="mount-path">
207-
<TextInput
208-
name="mountPath"
209-
isRequired
210-
type="text"
211-
value={formData.mountPath}
212-
onChange={(_, val) => setFormData({ ...formData, mountPath: val })}
213-
id="mount-path"
214-
/>
215-
</ThemeAwareFormGroupWrapper>
216-
<ThemeAwareFormGroupWrapper
217-
label="Default Mode"
218-
isRequired
219-
fieldId="default-mode"
220-
helperTextNode={
221-
!isDefaultModeValid ? (
222-
<HelperText>
223-
<HelperTextItem variant="error">
224-
Must be a valid UNIX file system permission value (i.e. 644)
225-
</HelperTextItem>
226-
</HelperText>
227-
) : null
228-
}
229-
>
230-
<TextInput
231-
name="defaultMode"
232-
isRequired
233-
type="text"
234-
value={defaultMode}
235-
validated={!isDefaultModeValid ? ValidatedOptions.error : undefined}
236-
onChange={(_, val) => handleDefaultModeInput(val)}
237-
id="default-mode"
238-
/>
239-
</ThemeAwareFormGroupWrapper>
240-
</Form>
241-
</ModalBody>
242-
<ModalFooter>
243-
<Button
244-
key="confirm"
245-
variant="primary"
246-
onClick={handleAddOrEditSubmit}
247-
isDisabled={!isDefaultModeValid}
248-
>
249-
{editIndex !== null ? 'Save' : 'Create'}
250-
</Button>
251-
<Button key="cancel" variant="link" onClick={clearForm}>
252-
Cancel
253-
</Button>
254-
</ModalFooter>
255-
</Modal>
110+
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
111+
<Button
112+
variant="primary"
113+
onClick={() => setIsApiCreateModalOpen(true)}
114+
style={{ width: 'fit-content' }}
115+
>
116+
Create New Secret
117+
</Button>
118+
</div>
119+
120+
{/* <SecretsAttachModal
121+
isOpen={isAttachModalOpen}
122+
setIsOpen={setIsAttachModalOpen}
123+
onClose={handleAttachSecrets}
124+
selectedSecrets={selectedSecretNames}
125+
availableSecrets={availableSecrets}
126+
initialMountPath={attachedMountPath}
127+
initialDefaultMode={attachedDefaultMode}
128+
/> */}
129+
130+
<SecretsApiCreateModal isOpen={isApiCreateModalOpen} setIsOpen={setIsApiCreateModalOpen} />
131+
256132
<Modal
257133
isOpen={isDeleteModalOpen}
258134
onClose={() => setIsDeleteModalOpen(false)}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { Button } from '@patternfly/react-core/dist/esm/components/Button';
3+
import { Form } from '@patternfly/react-core/dist/esm/components/Form';
4+
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
5+
import { Grid, GridItem } from '@patternfly/react-core/dist/esm/layouts/Grid';
6+
import { MinusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/minus-circle-icon';
7+
import ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper';
8+
import PasswordInput from '~/shared/components/PasswordInput';
9+
10+
interface SecretKeyValuePairInputProps {
11+
index: number;
12+
keyValue: string;
13+
valueValue: string;
14+
onKeyChange: (value: string) => void;
15+
onValueChange: (value: string) => void;
16+
onRemove: () => void;
17+
canRemove: boolean;
18+
}
19+
20+
/**
21+
* A composite component for managing a single secret key-value pair
22+
* Handles layout and semantics for Key input, Value input, and Remove button
23+
*/
24+
const SecretKeyValuePairInput: React.FC<SecretKeyValuePairInputProps> = ({
25+
index,
26+
keyValue,
27+
valueValue,
28+
onKeyChange,
29+
onValueChange,
30+
onRemove,
31+
canRemove,
32+
}) => (
33+
<>
34+
<Grid hasGutter data-testid="key-value-pair" className="secret-key-grid">
35+
<GridItem span={11}>
36+
<Form>
37+
<ThemeAwareFormGroupWrapper isRequired label="Key" fieldId={`key-${index}`}>
38+
<TextInput
39+
id={`key-${index}`}
40+
data-testid="key-input"
41+
isRequired
42+
aria-label={`key of item ${index}`}
43+
value={keyValue}
44+
onChange={(_event, val) => onKeyChange(val)}
45+
/>
46+
</ThemeAwareFormGroupWrapper>
47+
</Form>
48+
</GridItem>
49+
<GridItem span={1} className="secret-remove-button-container">
50+
<Button
51+
isDisabled={!canRemove}
52+
data-testid="remove-key-value-pair"
53+
aria-label="Remove key-value pair"
54+
variant="plain"
55+
icon={<MinusCircleIcon />}
56+
onClick={onRemove}
57+
/>
58+
</GridItem>
59+
</Grid>
60+
<ThemeAwareFormGroupWrapper
61+
isRequired
62+
label="Value"
63+
fieldId={`value-${index}`}
64+
className="secret-value-indented"
65+
>
66+
<PasswordInput
67+
data-testid="value-input"
68+
isRequired
69+
aria-label={`value of item ${index}`}
70+
value={valueValue}
71+
onChange={(_event, val) => onValueChange(val)}
72+
/>
73+
</ThemeAwareFormGroupWrapper>
74+
</>
75+
);
76+
77+
export default SecretKeyValuePairInput;

0 commit comments

Comments
 (0)