@@ -8,65 +8,149 @@ import {
88 ModalVariant ,
99} from '@patternfly/react-core/dist/esm/components/Modal' ;
1010import { MultiTypeaheadSelect , MultiTypeaheadSelectOption } from '@patternfly/react-templates' ;
11- import { Form , FormGroup } from '@patternfly/react-core/dist/esm/components/Form' ;
11+ import { Form } from '@patternfly/react-core/dist/esm/components/Form' ;
1212import { HelperText , HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText' ;
1313import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput' ;
1414import { ValidatedOptions } from '@patternfly/react-core/helpers' ;
15+ import { Flex , FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex' ;
16+ import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip' ;
17+ import { InfoCircleIcon } from '@patternfly/react-icons/dist/esm/icons/info-circle-icon' ;
18+ import { Truncate } from '@patternfly/react-core/dist/esm/components/Truncate' ;
19+ import { Stack , StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack' ;
1520import { SecretsSecretListItem } from '~/generated/data-contracts' ;
1621import { isValidDefaultMode } from '~/app/pages/Workspaces/Form/helpers' ;
22+ import ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper' ;
1723
1824export interface SecretsAttachModalProps {
1925 isOpen : boolean ;
2026 setIsOpen : ( isOpen : boolean ) => void ;
2127 onClose : ( secrets : SecretsSecretListItem [ ] , mountPath : string , mode : number ) => void ;
22- selectedSecrets : string [ ] ;
2328 availableSecrets : SecretsSecretListItem [ ] ;
24- initialMountPath ?: string ;
25- initialDefaultMode ?: string ;
29+ existingSecretKeys : Set < string > ;
2630}
2731
32+ const DEFAULT_MODE_OCTAL = ( 420 ) . toString ( 8 ) ;
33+
2834export const SecretsAttachModal : React . FC < SecretsAttachModalProps > = ( {
2935 isOpen,
3036 setIsOpen,
3137 onClose,
32- selectedSecrets,
3338 availableSecrets,
34- initialMountPath = '' ,
35- initialDefaultMode = '' ,
39+ existingSecretKeys,
3640} ) => {
37- const [ selected , setSelected ] = useState < string [ ] > ( selectedSecrets ) ;
38- const [ mountPath , setMountPath ] = useState ( initialMountPath ) ;
39- const [ defaultMode , setDefaultMode ] = useState ( initialDefaultMode ) ;
41+ const [ selected , setSelected ] = useState < string [ ] > ( [ ] ) ;
42+ const [ mountPath , setMountPath ] = useState ( '' ) ;
43+ const [ defaultMode , setDefaultMode ] = useState ( DEFAULT_MODE_OCTAL ) ;
4044 const [ isDefaultModeValid , setIsDefaultModeValid ] = useState ( true ) ;
45+ const [ error , setError ] = useState < string > ( '' ) ;
4146
42- // Sync state with props when modal opens or props change
47+ // Reset state when modal opens
4348 useEffect ( ( ) => {
4449 if ( isOpen ) {
45- setSelected ( selectedSecrets ) ;
46- setMountPath ( initialMountPath ) ;
47- setDefaultMode ( initialDefaultMode ) ;
50+ setSelected ( [ ] ) ;
51+ setMountPath ( '' ) ;
52+ setDefaultMode ( DEFAULT_MODE_OCTAL ) ;
4853 setIsDefaultModeValid ( true ) ;
54+ setError ( '' ) ;
4955 }
50- } , [ isOpen , selectedSecrets , initialMountPath , initialDefaultMode ] ) ;
56+ } , [ isOpen ] ) ;
57+
58+ const getSecretKey = ( secretName : string , path : string , mode : number ) : string =>
59+ `${ secretName } :${ path } :${ mode } ` ;
5160
5261 const handleDefaultModeChange = ( val : string ) => {
5362 if ( val . length <= 3 ) {
5463 setDefaultMode ( val ) ;
5564 const isValid = isValidDefaultMode ( val ) ;
5665 setIsDefaultModeValid ( val . length === 3 && isValid ) ;
66+ setError ( '' ) ; // Clear error when user modifies input
67+ }
68+ } ;
69+
70+ const handleAttach = ( ) => {
71+ const mode = parseInt ( defaultMode , 8 ) ;
72+
73+ // Check for duplicates
74+ const duplicates : string [ ] = [ ] ;
75+ selected . forEach ( ( secretName ) => {
76+ const key = getSecretKey ( secretName , mountPath . trim ( ) , mode ) ;
77+ if ( existingSecretKeys . has ( key ) ) {
78+ duplicates . push ( secretName ) ;
79+ }
80+ } ) ;
81+
82+ if ( duplicates . length > 0 ) {
83+ const secretList = duplicates . join ( ', ' ) ;
84+ setError (
85+ `The following secret${ duplicates . length > 1 ? 's are' : ' is' } already mounted to "${ mountPath . trim ( ) } " with mode ${ defaultMode } : ${ secretList } ` ,
86+ ) ;
87+ return ;
5788 }
89+
90+ // No duplicates, proceed with attaching
91+ onClose (
92+ availableSecrets . filter ( ( secret ) => selected . includes ( secret . name ) ) ,
93+ mountPath . trim ( ) ,
94+ mode ,
95+ ) ;
5896 } ;
5997
6098 const initialOptions = useMemo < MultiTypeaheadSelectOption [ ] > (
6199 ( ) =>
62100 availableSecrets . map ( ( secret ) => ( {
63101 content : secret . name ,
64102 value : secret . name ,
65- selected : selectedSecrets . includes ( secret . name ) ,
66103 isDisabled : ! secret . canMount ,
67- description : `Type: ${ secret . type } ` ,
104+ description : (
105+ // <Grid style={{ maxWidth: '45vw' }}>
106+ < Flex justifyContent = { { default : 'justifyContentSpaceBetween' } } >
107+ < FlexItem style = { { maxWidth : '35vw' } } >
108+ < Stack >
109+ < StackItem >
110+ Type: { secret . type }
111+ { secret . immutable && '. Immutable' }
112+ </ StackItem >
113+ { secret . mounts && (
114+ < StackItem >
115+ { `Mounted to: ` }
116+ < Truncate
117+ content = { secret . mounts . map ( ( mount ) => mount . name ) . join ( ', ' ) }
118+ position = "middle"
119+ />
120+ </ StackItem >
121+ ) }
122+ </ Stack >
123+ </ FlexItem >
124+ < FlexItem >
125+ { secret . canMount && (
126+ < Tooltip
127+ aria = "none"
128+ aria-live = "polite"
129+ content = < Stack >
130+ < StackItem >
131+ Created at: { new Date ( secret . audit . createdAt ) . toLocaleString ( ) } { `by ` }
132+ { secret . audit . createdBy }
133+ </ StackItem >
134+ < StackItem >
135+ Updated at: { new Date ( secret . audit . updatedAt ) . toLocaleString ( ) } { `by ` }
136+ { secret . audit . updatedBy }
137+ </ StackItem >
138+ </ Stack >
139+ >
140+ < Button
141+ aria-label = "Show secret details"
142+ variant = "plain"
143+ id = "tt-ref"
144+ icon = { < InfoCircleIcon /> }
145+ />
146+ </ Tooltip >
147+ ) }
148+ </ FlexItem >
149+ </ Flex >
150+ // </Grid>
151+ ) ,
68152 } ) ) ,
69- [ availableSecrets , selectedSecrets ] ,
153+ [ availableSecrets ] ,
70154 ) ;
71155
72156 return (
@@ -81,26 +165,32 @@ export const SecretsAttachModal: React.FC<SecretsAttachModalProps> = ({
81165 < ModalHeader title = "Attach Existing Secrets" labelId = "basic-modal-title" />
82166 < ModalBody id = "modal-box-body-basic" >
83167 < Form >
84- < FormGroup label = "Secret" fieldId = "secret-select" >
168+ < ThemeAwareFormGroupWrapper label = "Secret" fieldId = "secret-select" >
85169 < MultiTypeaheadSelect
86170 initialOptions = { initialOptions }
87171 id = "secret-select"
88172 placeholder = "Select a secret"
89173 noOptionsFoundMessage = { ( filter ) => `No secret was found for "${ filter } "` }
90- onSelectionChange = { ( _ev , selections ) => setSelected ( selections as string [ ] ) }
174+ onSelectionChange = { ( _ev , selections ) => {
175+ setSelected ( selections as string [ ] ) ;
176+ setError ( '' ) ;
177+ } }
91178 />
92- </ FormGroup >
93- < FormGroup label = "Mount Path" isRequired fieldId = "mount-path" >
179+ </ ThemeAwareFormGroupWrapper >
180+ < ThemeAwareFormGroupWrapper label = "Mount Path" isRequired fieldId = "mount-path" >
94181 < TextInput
95182 name = "mountPath"
96183 isRequired
97184 type = "text"
98185 value = { mountPath }
99- onChange = { ( _ , val ) => setMountPath ( val ) }
186+ onChange = { ( _ , val ) => {
187+ setMountPath ( val ) ;
188+ setError ( '' ) ;
189+ } }
100190 id = "mount-path"
101191 />
102- </ FormGroup >
103- < FormGroup label = "Default Mode" isRequired fieldId = "default-mode" >
192+ </ ThemeAwareFormGroupWrapper >
193+ < ThemeAwareFormGroupWrapper label = "Default Mode" isRequired fieldId = "default-mode" >
104194 < TextInput
105195 name = "defaultMode"
106196 isRequired
@@ -117,21 +207,20 @@ export const SecretsAttachModal: React.FC<SecretsAttachModalProps> = ({
117207 </ HelperTextItem >
118208 </ HelperText >
119209 ) }
120- </ FormGroup >
210+ </ ThemeAwareFormGroupWrapper >
121211 </ Form >
212+ { error && (
213+ < HelperText >
214+ < HelperTextItem variant = "error" > { error } </ HelperTextItem >
215+ </ HelperText >
216+ ) }
122217 </ ModalBody >
123218 < ModalFooter >
124219 < Button
125220 key = "attach"
126221 variant = "primary"
127222 isDisabled = { ! isDefaultModeValid || ! mountPath || selected . length === 0 }
128- onClick = { ( ) =>
129- onClose (
130- availableSecrets . filter ( ( secret ) => selected . includes ( secret . name ) ) ,
131- mountPath ,
132- parseInt ( defaultMode , 8 ) ,
133- )
134- }
223+ onClick = { handleAttach }
135224 >
136225 Attach
137226 </ Button >
0 commit comments