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,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,79 +195,34 @@ export const WorkspaceFormPropertiesSecrets: React.FC<WorkspaceFormPropertiesSec
173195 </ Table >
174196 ) }
175197 < Button
176- variant = "primary"
177- icon = { < PlusCircleIcon /> }
178- 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 ) }
179207 style = { { marginTop : '1rem' , width : 'fit-content' } }
180208 >
181209 Create Secret
182210 </ 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 >
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+ />
249226 < Modal
250227 isOpen = { isDeleteModalOpen }
251228 onClose = { ( ) => setIsDeleteModalOpen ( false ) }
0 commit comments