1- import React , { useCallback , useState } from 'react' ;
1+ import React , { useCallback , useEffect , useState } from 'react' ;
22import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon' ;
33import {
44 Table ,
@@ -12,20 +12,17 @@ import {
1212import { Button } from '@patternfly/react-core/dist/esm/components/Button' ;
1313import {
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' ;
2219import { Dropdown , DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown' ;
2320import { 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' ;
2621import { SecretsSecretListItem , WorkspacesPodSecretMount } from '~/generated/data-contracts' ;
2722import { useNotebookAPI } from '~/app/hooks/useNotebookAPI' ;
2823import { useNamespaceContext } from '~/app/context/NamespaceContextProvider' ;
24+ import { SecretsAttachModal } from './secrets/SecretsAttachModal' ;
25+ import { SecretsCreateModal } from './secrets/SecretsCreateModal' ;
2926
3027interface 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