github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/application-summary/edit-notification-subscriptions.tsx (about) 1 import {Autocomplete} from 'argo-ui'; 2 import * as React from 'react'; 3 import {DataLoader} from '../../../shared/components'; 4 import * as models from '../../../shared/models'; 5 import {services} from '../../../shared/services'; 6 7 import {ApplicationSummaryProps} from './application-summary'; 8 9 import './edit-notification-subscriptions.scss'; 10 11 export const NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX = 'notifications.argoproj.io/subscribe'; 12 13 export const NOTIFICATION_SUBSCRIPTION_ANNOTATION_REGEX = new RegExp(`^notifications\\.argoproj\\.io/subscribe\\.[a-zA-Z-]{1,100}\\.[a-zA-Z-]{1,100}$`); 14 15 export type TNotificationSubscription = { 16 trigger: string; 17 // notification service name 18 service: string; 19 // a semicolon separated list of recipients 20 value: string; 21 }; 22 23 export const notificationSubscriptionsParser = { 24 annotationsToSubscriptions: (annotations: models.Application['metadata']['annotations']): TNotificationSubscription[] => { 25 const subscriptions: TNotificationSubscription[] = []; 26 27 for (const [key, value] of Object.entries(annotations || {})) { 28 if (NOTIFICATION_SUBSCRIPTION_ANNOTATION_REGEX.test(key)) { 29 try { 30 const [trigger, service] = key.slice(NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX.length + 1 /* for dot "." */).split('.'); 31 32 subscriptions.push({trigger, service, value}); 33 } catch (e) { 34 // console.error(`annotationsToSubscriptions parsing issue for ${key}`); 35 throw new Error(e); 36 } 37 } 38 } 39 40 return subscriptions; 41 }, 42 subscriptionsToAnnotations: (subscriptions: TNotificationSubscription[]): models.Application['metadata']['annotations'] => { 43 const annotations: models.Application['metadata']['annotations'] = {}; 44 45 for (const subscription of subscriptions || []) { 46 annotations[notificationSubscriptionsParser.subscriptionToAnnotationKey(subscription)] = subscription.value; 47 } 48 49 return annotations; 50 }, 51 subscriptionToAnnotationKey: (subscription: TNotificationSubscription): string => 52 `${NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX}.${subscription.trigger}.${subscription.service}` 53 }; 54 55 /** 56 * split the notification subscription related annotation to have it in seperate edit field 57 * this hook will emit notification subscription state, controller & merge utility to core annotations helpful when final submit 58 */ 59 export const useEditNotificationSubscriptions = (annotations: models.Application['metadata']['annotations']) => { 60 const [subscriptions, setSubscriptions] = React.useState(notificationSubscriptionsParser.annotationsToSubscriptions(annotations)); 61 62 const onAddNewSubscription = () => { 63 const lastSubscription = subscriptions[subscriptions.length - 1]; 64 65 if (subscriptions.length === 0 || lastSubscription.trigger || lastSubscription.service || lastSubscription.value) { 66 setSubscriptions([ 67 ...subscriptions, 68 { 69 trigger: '', 70 service: '', 71 value: '' 72 } 73 ]); 74 } 75 }; 76 77 const onEditSubscription = (idx: number, subscription: TNotificationSubscription) => { 78 const existingSubscription = subscriptions.findIndex((sub, toFindIdx) => toFindIdx !== idx && sub.service === subscription.service && sub.trigger === subscription.trigger); 79 let newSubscriptions = [...subscriptions]; 80 81 if (existingSubscription !== -1) { 82 // remove existing subscription 83 newSubscriptions = newSubscriptions.filter((_, newSubscriptionIdx) => newSubscriptionIdx !== existingSubscription); 84 // decrement index because one value is removed 85 idx--; 86 } 87 88 if (idx === -1) { 89 newSubscriptions = [subscription]; 90 } else { 91 newSubscriptions = newSubscriptions.map((oldSubscription, oldSubscriptionIdx) => (oldSubscriptionIdx === idx ? subscription : oldSubscription)); 92 } 93 94 setSubscriptions(newSubscriptions); 95 }; 96 97 const onRemoveSubscription = (idx: number) => idx >= 0 && setSubscriptions(subscriptions.filter((_, i) => i !== idx)); 98 99 const withNotificationSubscriptions = 100 (updateApp: ApplicationSummaryProps['updateApp']) => 101 (...args: Parameters<ApplicationSummaryProps['updateApp']>) => { 102 const app = args[0]; 103 104 const notificationSubscriptionsRaw = notificationSubscriptionsParser.subscriptionsToAnnotations(subscriptions); 105 106 if (Object.keys(notificationSubscriptionsRaw)?.length) { 107 app.metadata.annotations = { 108 ...notificationSubscriptionsRaw, 109 ...(app.metadata.annotations || {}) 110 }; 111 } 112 113 return updateApp(app, args[1]); 114 }; 115 116 const onResetNotificationSubscriptions = () => setSubscriptions(notificationSubscriptionsParser.annotationsToSubscriptions(annotations)); 117 118 return { 119 /** 120 * abstraction of notification subscription annotations in edit view 121 */ 122 subscriptions, 123 onAddNewSubscription, 124 onEditSubscription, 125 onRemoveSubscription, 126 /** 127 * merge abstracted 'subscriptions' into core 'metadata.annotations' in form submit 128 */ 129 withNotificationSubscriptions, 130 onResetNotificationSubscriptions 131 }; 132 }; 133 134 export interface EditNotificationSubscriptionsProps extends ReturnType<typeof useEditNotificationSubscriptions> {} 135 136 export const EditNotificationSubscriptions = ({subscriptions, onAddNewSubscription, onEditSubscription, onRemoveSubscription}: EditNotificationSubscriptionsProps) => { 137 return ( 138 <div className='edit-notification-subscriptions argo-field'> 139 {subscriptions.map((subscription, idx) => ( 140 <div className='edit-notification-subscriptions__subscription' key={idx}> 141 <input className='argo-field edit-notification-subscriptions__input-prefix' disabled={true} value={NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX} /> 142 <b> . </b> 143 <DataLoader load={() => services.notification.listTriggers().then(triggers => triggers.map(trigger => trigger.name))}> 144 {triggersList => ( 145 <Autocomplete 146 wrapperProps={{ 147 className: 'argo-field edit-notification-subscriptions__autocomplete-wrapper' 148 }} 149 inputProps={{ 150 className: 'argo-field', 151 placeholder: 'on-sync-running', 152 title: 'Trigger' 153 }} 154 value={subscription.trigger} 155 onChange={e => { 156 onEditSubscription(idx, { 157 ...subscription, 158 trigger: e.target.value 159 }); 160 }} 161 items={triggersList} 162 onSelect={trigger => onEditSubscription(idx, {...subscription, trigger})} 163 filterSuggestions={true} 164 qeid='application-edit-notification-subscription-trigger' 165 /> 166 )} 167 </DataLoader> 168 <b> . </b> 169 <DataLoader load={() => services.notification.listServices().then(_services => _services.map(service => service.name))}> 170 {serviceList => ( 171 <Autocomplete 172 wrapperProps={{ 173 className: 'argo-field edit-notification-subscriptions__autocomplete-wrapper' 174 }} 175 inputProps={{ 176 className: 'argo-field', 177 placeholder: 'slack', 178 title: 'Service' 179 }} 180 value={subscription.service} 181 onChange={e => { 182 onEditSubscription(idx, { 183 ...subscription, 184 service: e.target.value 185 }); 186 }} 187 items={serviceList} 188 onSelect={service => onEditSubscription(idx, {...subscription, service})} 189 filterSuggestions={true} 190 qeid='application-edit-notification-subscription-service' 191 /> 192 )} 193 </DataLoader> 194 = 195 <input 196 autoComplete='fake' 197 className='argo-field' 198 placeholder='my-channel1; my-channel2' 199 title='Value' 200 value={subscription.value} 201 onChange={e => { 202 onEditSubscription(idx, { 203 ...subscription, 204 value: e.target.value 205 }); 206 }} 207 qe-id='application-edit-notification-subscription-value' 208 /> 209 <button className='button-close'> 210 <i className='fa fa-times' style={{cursor: 'pointer'}} onClick={() => onRemoveSubscription(idx)} /> 211 </button> 212 </div> 213 ))} 214 {subscriptions.length === 0 && <label>No items</label>} 215 <div> 216 <button className='argo-button argo-button--base argo-button--short' onClick={() => onAddNewSubscription()}> 217 <i className='fa fa-plus' style={{cursor: 'pointer'}} /> 218 </button> 219 </div> 220 </div> 221 ); 222 };