-
Notifications
You must be signed in to change notification settings - Fork 5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat(ui): notification subscriptions edit field > this new field is just an abstraction of relevant annotations Signed-off-by: Mayursinh Sarvaiya <marvinduff97@gmail.com> * fix: codeql regex issue Signed-off-by: Mayursinh Sarvaiya <marvinduff97@gmail.com> Signed-off-by: Mayursinh Sarvaiya <marvinduff97@gmail.com>
- Loading branch information
Showing
8 changed files
with
344 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
ui/src/app/applications/components/application-summary/edit-annotations.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import * as React from 'react'; | ||
import {FormField} from 'argo-ui'; | ||
import {FormApi} from 'react-form'; | ||
import * as models from '../../../shared/models'; | ||
import {MapInputField} from '../../../shared/components'; | ||
import {notificationSubscriptionsParser} from './edit-notification-subscriptions'; | ||
|
||
export const EditAnnotations = (props: {formApi: FormApi; app: models.Application}) => { | ||
const once = React.useRef(false); | ||
|
||
const removeNotificationSubscriptionRelatedAnnotations = () => { | ||
const notificationSubscriptions = notificationSubscriptionsParser.annotationsToSubscriptions(props.app.metadata.annotations); | ||
|
||
if (notificationSubscriptions.length > 0) { | ||
const annotationsWithoutNotificationSubscriptions = props.app.metadata.annotations || {}; | ||
|
||
for (const notificationSubscriptionAnnotation of notificationSubscriptions) { | ||
const key = notificationSubscriptionsParser.subscriptionToAnnotationKey(notificationSubscriptionAnnotation); | ||
|
||
delete annotationsWithoutNotificationSubscriptions[key]; | ||
} | ||
|
||
props.formApi.setValue('metadata.annotations', annotationsWithoutNotificationSubscriptions); | ||
} | ||
}; | ||
|
||
if (!once.current) { | ||
once.current = true; | ||
removeNotificationSubscriptionRelatedAnnotations(); | ||
} | ||
|
||
return <FormField formApi={props.formApi} field='metadata.annotations' component={MapInputField} />; | ||
}; |
28 changes: 28 additions & 0 deletions
28
ui/src/app/applications/components/application-summary/edit-notification-subscriptions.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
.edit-notification-subscriptions { | ||
border-bottom: none; | ||
|
||
&__subscription { | ||
display: flex; | ||
align-items: flex-end; | ||
gap: 8px; | ||
margin-bottom: 12px; | ||
} | ||
|
||
&__autocomplete-wrapper { | ||
border-bottom: none; | ||
} | ||
|
||
&__input-prefix { | ||
font-size: 12px; | ||
width: fit-content !important; | ||
} | ||
|
||
input.argo-field + div { | ||
z-index: 100 !important; | ||
} | ||
|
||
.button-close { | ||
margin-top: auto !important; | ||
margin-bottom: auto !important; | ||
} | ||
} |
220 changes: 220 additions & 0 deletions
220
ui/src/app/applications/components/application-summary/edit-notification-subscriptions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
import {Autocomplete} from 'argo-ui'; | ||
import * as React from 'react'; | ||
import {DataLoader} from '../../../shared/components'; | ||
import * as models from '../../../shared/models'; | ||
import {services} from '../../../shared/services'; | ||
|
||
import {ApplicationSummaryProps} from './application-summary'; | ||
|
||
require('./edit-notification-subscriptions.scss'); | ||
|
||
export const NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX = 'notifications.argoproj.io/subscribe'; | ||
|
||
export const NOTIFICATION_SUBSCRIPTION_ANNOTATION_REGEX = new RegExp(`^notifications\.argoproj\.io\/subscribe\.[a-zA-Z-]{1,100}\.[a-zA-Z-]{1,100}$`); | ||
|
||
export type TNotificationSubscription = { | ||
trigger: string; | ||
// notification service name | ||
service: string; | ||
// a semicolon separated list of recipients | ||
value: string; | ||
}; | ||
|
||
export const notificationSubscriptionsParser = { | ||
annotationsToSubscriptions: (annotations: models.Application['metadata']['annotations']): TNotificationSubscription[] => { | ||
const subscriptions: TNotificationSubscription[] = []; | ||
|
||
for (const [key, value] of Object.entries(annotations || {})) { | ||
if (NOTIFICATION_SUBSCRIPTION_ANNOTATION_REGEX.test(key)) { | ||
try { | ||
const [trigger, service] = key.slice(NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX.length + 1 /* for dot "." */).split('.'); | ||
|
||
subscriptions.push({trigger, service, value}); | ||
} catch (e) { | ||
// console.error(`annotationsToSubscriptions parsing issue for ${key}`); | ||
throw new Error(e); | ||
} | ||
} | ||
} | ||
|
||
return subscriptions; | ||
}, | ||
subscriptionsToAnnotations: (subscriptions: TNotificationSubscription[]): models.Application['metadata']['annotations'] => { | ||
const annotations: models.Application['metadata']['annotations'] = {}; | ||
|
||
for (const subscription of subscriptions || []) { | ||
annotations[notificationSubscriptionsParser.subscriptionToAnnotationKey(subscription)] = subscription.value; | ||
} | ||
|
||
return annotations; | ||
}, | ||
subscriptionToAnnotationKey: (subscription: TNotificationSubscription): string => | ||
`${NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX}.${subscription.trigger}.${subscription.service}` | ||
}; | ||
|
||
/** | ||
* split the notification subscription related annotation to have it in seperate edit field | ||
* this hook will emit notification subscription state, controller & merge utility to core annotations helpful when final submit | ||
*/ | ||
export const useEditNotificationSubscriptions = (annotations: models.Application['metadata']['annotations']) => { | ||
const [subscriptions, setSubscriptions] = React.useState(notificationSubscriptionsParser.annotationsToSubscriptions(annotations)); | ||
|
||
const onAddNewSubscription = () => { | ||
const lastSubscription = subscriptions[subscriptions.length - 1]; | ||
|
||
if (subscriptions.length === 0 || lastSubscription.trigger || lastSubscription.service || lastSubscription.value) { | ||
setSubscriptions([ | ||
...subscriptions, | ||
{ | ||
trigger: '', | ||
service: '', | ||
value: '' | ||
} | ||
]); | ||
} | ||
}; | ||
|
||
const onEditSubscription = (idx: number, subscription: TNotificationSubscription) => { | ||
const existingSubscription = subscriptions.findIndex((sub, toFindIdx) => toFindIdx !== idx && sub.service === subscription.service && sub.trigger === subscription.trigger); | ||
let newSubscriptions = [...subscriptions]; | ||
|
||
if (existingSubscription !== -1) { | ||
// remove existing subscription | ||
newSubscriptions = newSubscriptions.filter((_, newSubscriptionIdx) => newSubscriptionIdx !== existingSubscription); | ||
// decrement index because one value is removed | ||
idx--; | ||
} | ||
|
||
if (idx === -1) { | ||
newSubscriptions = [subscription]; | ||
} else { | ||
newSubscriptions = newSubscriptions.map((oldSubscription, oldSubscriptionIdx) => (oldSubscriptionIdx === idx ? subscription : oldSubscription)); | ||
} | ||
|
||
setSubscriptions(newSubscriptions); | ||
}; | ||
|
||
const onRemoveSubscription = (idx: number) => idx >= 0 && setSubscriptions(subscriptions.filter((_, i) => i !== idx)); | ||
|
||
const withNotificationSubscriptions = (updateApp: ApplicationSummaryProps['updateApp']) => (...args: Parameters<ApplicationSummaryProps['updateApp']>) => { | ||
const app = args[0]; | ||
|
||
const notificationSubscriptionsRaw = notificationSubscriptionsParser.subscriptionsToAnnotations(subscriptions); | ||
|
||
if (Object.keys(notificationSubscriptionsRaw)?.length) { | ||
app.metadata.annotations = { | ||
...notificationSubscriptionsRaw, | ||
...(app.metadata.annotations || {}) | ||
}; | ||
} | ||
|
||
return updateApp(app, args[1]); | ||
}; | ||
|
||
const onResetNotificationSubscriptions = () => setSubscriptions(notificationSubscriptionsParser.annotationsToSubscriptions(annotations)); | ||
|
||
return { | ||
/** | ||
* abstraction of notification subscription annotations in edit view | ||
*/ | ||
subscriptions, | ||
onAddNewSubscription, | ||
onEditSubscription, | ||
onRemoveSubscription, | ||
/** | ||
* merge abstracted 'subscriptions' into core 'metadata.annotations' in form submit | ||
*/ | ||
withNotificationSubscriptions, | ||
onResetNotificationSubscriptions | ||
}; | ||
}; | ||
|
||
export interface EditNotificationSubscriptionsProps extends ReturnType<typeof useEditNotificationSubscriptions> {} | ||
|
||
export const EditNotificationSubscriptions = ({subscriptions, onAddNewSubscription, onEditSubscription, onRemoveSubscription}: EditNotificationSubscriptionsProps) => { | ||
return ( | ||
<div className='edit-notification-subscriptions argo-field'> | ||
{subscriptions.map((subscription, idx) => ( | ||
<div className='edit-notification-subscriptions__subscription' key={idx}> | ||
<input className='argo-field edit-notification-subscriptions__input-prefix' disabled={true} value={NOTIFICATION_SUBSCRIPTION_ANNOTATION_PREFIX} /> | ||
<b> . </b> | ||
<DataLoader load={() => services.notification.listTriggers().then(triggers => triggers.map(trigger => trigger.name))}> | ||
{triggersList => ( | ||
<Autocomplete | ||
wrapperProps={{ | ||
className: 'argo-field edit-notification-subscriptions__autocomplete-wrapper' | ||
}} | ||
inputProps={{ | ||
className: 'argo-field', | ||
placeholder: 'on-sync-running', | ||
title: 'Trigger' | ||
}} | ||
value={subscription.trigger} | ||
onChange={e => { | ||
onEditSubscription(idx, { | ||
...subscription, | ||
trigger: e.target.value | ||
}); | ||
}} | ||
items={triggersList} | ||
onSelect={trigger => onEditSubscription(idx, {...subscription, trigger})} | ||
filterSuggestions={true} | ||
qeid='application-edit-notification-subscription-trigger' | ||
/> | ||
)} | ||
</DataLoader> | ||
<b> . </b> | ||
<DataLoader load={() => services.notification.listServices().then(_services => _services.map(service => service.name))}> | ||
{serviceList => ( | ||
<Autocomplete | ||
wrapperProps={{ | ||
className: 'argo-field edit-notification-subscriptions__autocomplete-wrapper' | ||
}} | ||
inputProps={{ | ||
className: 'argo-field', | ||
placeholder: 'slack', | ||
title: 'Service' | ||
}} | ||
value={subscription.service} | ||
onChange={e => { | ||
onEditSubscription(idx, { | ||
...subscription, | ||
service: e.target.value | ||
}); | ||
}} | ||
items={serviceList} | ||
onSelect={service => onEditSubscription(idx, {...subscription, service})} | ||
filterSuggestions={true} | ||
qeid='application-edit-notification-subscription-service' | ||
/> | ||
)} | ||
</DataLoader> | ||
= | ||
<input | ||
autoComplete='fake' | ||
className='argo-field' | ||
placeholder='my-channel1; my-channel2' | ||
title='Value' | ||
value={subscription.value} | ||
onChange={e => { | ||
onEditSubscription(idx, { | ||
...subscription, | ||
value: e.target.value | ||
}); | ||
}} | ||
qe-id='application-edit-notification-subscription-value' | ||
/> | ||
<button className='button-close'> | ||
<i className='fa fa-times' style={{cursor: 'pointer'}} onClick={() => onRemoveSubscription(idx)} /> | ||
</button> | ||
</div> | ||
))} | ||
{subscriptions.length === 0 && <label>No items</label>} | ||
<div> | ||
<button className='argo-button argo-button--base argo-button--short' onClick={() => onAddNewSubscription()}> | ||
<i className='fa fa-plus' style={{cursor: 'pointer'}} /> | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.