Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Update: Notifications editor #6438

Merged
merged 36 commits into from Sep 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
87cde3c
wip
gschueler Sep 8, 2020
7f071f9
Add dummy notification plugins for UI input properties
gschueler Sep 9, 2020
0d103a5
i18n update
gschueler Sep 9, 2020
cfbb65f
update notifications editor vue
gschueler Sep 9, 2020
59c73d5
util to show extended description
gschueler Sep 9, 2020
785268e
update description of avg duration threshold
gschueler Sep 9, 2020
0d809ed
link to editor css
gschueler Sep 9, 2020
c1eb0e5
unit tests for normalized map methods
gschueler Sep 9, 2020
6563463
fix: delete item
gschueler Sep 9, 2020
a9d7cec
fix: edit notification
gschueler Sep 9, 2020
043d726
Add Copy action
gschueler Sep 9, 2020
f762da8
change copy button text
gschueler Sep 9, 2020
4af9f18
remove internal props
gschueler Sep 9, 2020
8450e20
add tests for jobDefinitionNotifications
gschueler Sep 9, 2020
a0972ee
Enable: multiple notifications of same type/trigger using new input data
gschueler Sep 9, 2020
ed62899
fix: ajax validation should return 404 for missing plugin
gschueler Sep 9, 2020
fa142e2
add undo/redo component
gschueler Sep 10, 2020
35bd5b6
add undo/redo for notification changes
gschueler Sep 10, 2020
0027da5
remove debug json from content
gschueler Sep 10, 2020
d86d761
Enable rundeck.feature.notificationsEditorVue.enabled by default in d…
gschueler Sep 10, 2020
b2b6100
Fix brace require
ProTip Sep 14, 2020
468a340
Use a sep webpack lib name to avoid collision with ui project
ProTip Sep 14, 2020
8b401ce
Grammar
ProTip Sep 14, 2020
bf45c20
extend interface to allow ignoredScope parameter
gschueler Sep 15, 2020
3e0bdcc
plugin validate endpoint allows ignoredScope parameter
gschueler Sep 15, 2020
7b0d4bb
add ignoredScope param to validate method
gschueler Sep 15, 2020
d480168
add props scope/defaultScope, restricts display of properties
gschueler Sep 15, 2020
86d48e6
show instance scoped properties in view/config, and ignore project/fr…
gschueler Sep 15, 2020
edf50cb
fix: method call param
gschueler Sep 15, 2020
4453595
add hook to page warning when notifications are modified
gschueler Sep 16, 2020
f59b154
update layout: show all sections as headers, change button style
gschueler Sep 16, 2020
a4f5ff7
update placeholder text and help popover for average duration threshold
gschueler Sep 16, 2020
e1242e7
fix: dropdown and edit button can get split apart at media breakpoints
gschueler Sep 16, 2020
a7738c1
remove max width
gschueler Sep 16, 2020
1071e36
fix repeat on main section
gschueler Sep 16, 2020
8ec1d39
move actions dropdown to left side of notification
gschueler Sep 16, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -161,6 +161,15 @@ ValidatedPlugin validatePluginByName(String name, PluggableProviderService servi
* @return Map containing valid:true/false, and report: {@link com.dtolabs.rundeck.core.plugins.configuration.Validator.Report}
*/
ValidatedPlugin validatePluginByName(String name, PluggableProviderService service, Map instanceConfiguration);
/**
* Validate a provider for a service with an instance configuration
* @param name name of bean or provider
* @param service provider service
* @param instanceConfiguration config map
* @param ignoredScope scope to ignore
* @return Map containing valid:true/false, and report: {@link com.dtolabs.rundeck.core.plugins.configuration.Validator.Report}
*/
ValidatedPlugin validatePluginByName(String name, PluggableProviderService service, Map instanceConfiguration, PropertyScope ignoredScope) ;
/**
* Load a plugin instance with the given bean or provider name
* @param name name of bean or provider
Expand Down
1 change: 1 addition & 0 deletions rundeckapp/grails-app/conf/application.groovy
Expand Up @@ -34,6 +34,7 @@ environments {
rundeck.feature.cleanExecutionsHistoryJob.enabled = true
rundeck.feature.executionLifecyclePlugin.enabled = true
rundeck.feature.legacyExecOutputViewer.enabled = false
rundeck.feature.notificationsEditorVue.enabled = true
dataSource {
dbCreate = "create-drop" // one of 'create', 'create-drop','update'
url = "jdbc:h2:file:./db/devDb"
Expand Down
5 changes: 5 additions & 0 deletions rundeckapp/grails-app/conf/spring/resources.groovy
Expand Up @@ -48,6 +48,8 @@ import com.dtolabs.rundeck.server.plugins.logging.*
import com.dtolabs.rundeck.server.plugins.logs.*
import com.dtolabs.rundeck.server.plugins.logstorage.TreeExecutionFileStoragePlugin
import com.dtolabs.rundeck.server.plugins.logstorage.TreeExecutionFileStoragePluginFactory
import com.dtolabs.rundeck.server.plugins.notification.DummyEmailNotificationPlugin
import com.dtolabs.rundeck.server.plugins.notification.DummyWebhookNotificationPlugin
import com.dtolabs.rundeck.server.plugins.services.*
import com.dtolabs.rundeck.server.plugins.storage.DbStoragePlugin
import com.dtolabs.rundeck.server.plugins.storage.DbStoragePluginFactory
Expand Down Expand Up @@ -458,6 +460,9 @@ beans={
RenderDatatypeFilterPlugin,
QuietFilterPlugin,
HighlightFilterPlugin,
//dummy notification plugins
DummyEmailNotificationPlugin,
DummyWebhookNotificationPlugin,
].each {
"rundeckAppPlugin_${it.simpleName}"(PluginFactoryBean, it)
}
Expand Down
Expand Up @@ -2,6 +2,7 @@ package rundeck.controllers

import com.dtolabs.rundeck.app.support.PluginResourceReq
import com.dtolabs.rundeck.core.authorization.AuthContext
import com.dtolabs.rundeck.core.plugins.configuration.PropertyScope
import org.rundeck.app.spi.AuthorizedServicesProvider
import org.rundeck.core.auth.AuthConstants
import com.dtolabs.rundeck.core.plugins.PluginValidator
Expand Down Expand Up @@ -286,7 +287,33 @@ class PluginController extends ControllerBase {
config = request.JSON.config
}
config = ParamsUtil.cleanMap(config)
def validation = pluginService.validatePluginConfig(service, name, config)
PropertyScope ignoredScope=null
if(params.ignoredScope){
try{
ignoredScope=PropertyScope.valueOf(params.ignoredScope.toString())
} catch (IllegalArgumentException e) {
response.status = 400
return respond(
[status: 400, formats: ['json']],
(Object) [
error: g.message(
code: 'request.error.invalidrequest.message',
args: [params.ignoredScope]
)
]
)
}

}
def validation = pluginService.validatePluginConfig(service, name, config, ignoredScope)
if(!validation){
response.status=404

return render(contentType: 'application/json') {
valid false
delegate.error ('Provider not found for '+service+': '+name)
}
}
def errorsMap = validation.report.errors
def decomp = ParamsUtil.decomposeMap(errorsMap)
// System.err.println("config: $config, errors: $errorsMap, decomp: $decomp")
Expand Down
51 changes: 50 additions & 1 deletion rundeckapp/grails-app/domain/rundeck/Notification.groovy
Expand Up @@ -21,7 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper

/*
* Notification.java
*
*
* User: Greg Schueler <a href="mailto:greg@dtosolutions.com">greg@dtosolutions.com</a>
* Created: May 17, 2010 11:20:53 AM
* $Id$
Expand Down Expand Up @@ -153,6 +153,55 @@ public class Notification {
return null
}
}
public Map toNormalizedMap(){
if(type=='email'){
def configuration = mailConfiguration()
if(configuration.attachLog && configuration.attachLogInline){
configuration.attachType='inline'
}else if(configuration.attachLog && configuration.attachLogInFile){
configuration.attachType='file'
}
configuration.remove('attachLogInline')
configuration.remove('attachLogInFile')
return [type:'email', trigger:eventTrigger, config: configuration]
}else if(type=='url'){
return [type: 'url', trigger:eventTrigger, config: [urls: content, format: format]]
}else if(type){
def config=[:]
if(content){
config=this.configuration
}
return [type:type,trigger:eventTrigger,config:config]
}else{
return null
}
}

public static Notification fromNormalizedMap( Map data){
Notification n = new Notification(eventTrigger:data.trigger,type:data.type)
if(data.type=='email'){
def map=data.config
if(map['attachLog']) {
if (map.attachType=='inline') {
map['attachLogInline'] = true
}else{
map['attachLogInFile'] = true
}
map.remove('attachType')
}
n.configuration=map
}else if(data.type=='url'){
n.format=data.config.format
n.content=data.config.urls
}else if(data.type){
if(data.config && data.config instanceof Map){
n.configuration=data.config
}else{
n.content=null
}
}
return n;
}


public String toString ( ) {
Expand Down
13 changes: 7 additions & 6 deletions rundeckapp/grails-app/i18n/messages.properties
Expand Up @@ -751,12 +751,13 @@ scheduledExecution.property.timeout.description=The maximum time for an executio
or specify time units: "120m", "2h", "3d". Use blank or 0 to indicate no timeout. Can include option value \
references like "${option.timeout}".
scheduledExecution.property.timeout.label=Timeout
scheduledExecution.property.notifyAvgDurationThreshold.description=Add or set a threshold value to the avg duration in order to trigger this notification. Options: \
- percentage => eg: 20% \
- time delta => eg: +20s, +20 \
- absolute time => 30s, 5m \
Time in seconds if you don't specify time units \
Can include option value references like ${option.avgDurationThreshold}.
scheduledExecution.property.notifyAvgDurationThreshold.description=Optional duration threshold to trigger the notifications. If not specified, the Job Average duration will be used.\n\n\
- percentage of average: `20%`\n\
- time delta from the average: `+20s`, `+20`\n\
- absolute time: `30s`, `5m`\n\
Use `s`,`m`,`h`,`d`,`w`,`y` etc as time units for seconds, minutes, hours, etc.\n\
Unit will be seconds if it is not specified.\n\n\
Can include option value references like `${option.avgDurationThreshold}`.
scheduledExecution.property.notifyAvgDurationThreshold.label=Threshold
scheduledExecution.property.retry.label=Retry
scheduledExecution.property.retry.description=Maximum number of times to retry execution when this job is directly invoked. Retry will occur if the job fails or times out, but not if it is manually killed. Can use an option value reference like "${option.retry}".
Expand Down
Expand Up @@ -134,6 +134,19 @@ class PluginService implements ResourceFormats {
PluggableProviderService providerService = createPluggableService((Class) serviceType)
return rundeckPluginRegistry?.validatePluginByName(provider, providerService, config)
}
/**
* Configure a new plugin using a specific property resolver for configuration
* @param service service
* @param provider provider name
* @param config instance configuration data
* @param ignoredScope property scope to ignore
* @return validation
*/
def ValidatedPlugin validatePluginConfig(String service, String provider, Map config, PropertyScope ignoredScope) {
Class serviceType = getPluginTypeByService(service)
PluggableProviderService providerService = createPluggableService((Class) serviceType)
return rundeckPluginRegistry?.validatePluginByName(provider, providerService, config, ignoredScope)
}

/**
*
Expand Down
Expand Up @@ -23,9 +23,12 @@ import com.dtolabs.rundeck.core.authorization.UserAndRolesAuthContext
import com.dtolabs.rundeck.core.plugins.DescribedPlugin
import com.dtolabs.rundeck.core.plugins.JobLifecyclePluginException
import com.dtolabs.rundeck.core.plugins.ValidatedPlugin
import grails.converters.JSON
import grails.gorm.transactions.NotTransactional
import grails.orm.HibernateCriteriaBuilder
import groovy.transform.CompileStatic
import org.grails.web.json.JSONArray
import org.grails.web.json.JSONElement
import org.hibernate.criterion.DetachedCriteria
import org.hibernate.criterion.Projections
import org.hibernate.criterion.Subqueries
Expand Down Expand Up @@ -2906,10 +2909,21 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing
@CompileStatic
public void jobDefinitionNotifications(ScheduledExecution scheduledExecution, ScheduledExecution input,Map params, UserAndRoles userAndRoles) {
Collection<Notification> notificationSet=[]
boolean replaceAll=false
if(input){
if(input.notifications) {
notificationSet.addAll(input.notifications.collect{Notification.fromMap(it.eventTrigger,it.toMap())})
}
}else if(params.jobNotificationsJson){
def notificationsData = JSON.parse(params.jobNotificationsJson.toString())
if(notificationsData instanceof JSONArray){
replaceAll=true
for(Object item: notificationsData){
if(item instanceof JSONObject){
notificationSet.add(Notification.fromNormalizedMap(item))
}
}
}
}else if(params.notified != 'false'){
def notifications = parseParamNotifications(params)
notificationSet=notifications.collect {Map notif ->
Expand All @@ -2929,7 +2943,7 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing
def addedNotifications=[]
notificationSet.each{Notification n->
//modify existing notification
def oldn = scheduledExecution.findNotification(n.eventTrigger,n.type)
Notification oldn = replaceAll?null:scheduledExecution.findNotification(n.eventTrigger,n.type)
if(oldn){
oldn.content=n.content
oldn.format=n.format
Expand Down
Expand Up @@ -15,6 +15,13 @@
--}%

<%@ page import="rundeck.controllers.ScheduledExecutionController; com.dtolabs.rundeck.plugins.ServiceNameConstants" %>

<feature:enabled name="notificationsEditorVue">
<div class="job-editor-vue">
<app :event-bus="EventBus" />
</div>
</feature:enabled>
<feature:disabled name="notificationsEditorVue">
<g:set var="notifications" value="${scheduledExecution.notifications}"/>
<g:set var="defSuccess" value="${scheduledExecution.findNotification(ScheduledExecutionController.ONSUCCESS_TRIGGER_NAME, ScheduledExecutionController.EMAIL_NOTIFICATION_TYPE)}"/>
<g:set var="isSuccess" value="${'true' == params[ScheduledExecutionController.NOTIFY_ONSUCCESS_EMAIL] || null== params[ScheduledExecutionController.NOTIFY_ONSUCCESS_EMAIL] && defSuccess}"/>
Expand Down Expand Up @@ -170,3 +177,4 @@
adminauth: adminauth,
serviceName: ServiceNameConstants.Notification
]}"/>
</feature:disabled>
43 changes: 43 additions & 0 deletions rundeckapp/grails-app/views/scheduledExecution/create.gsp
Expand Up @@ -30,7 +30,50 @@
<asset:javascript src="util/yellowfade.js"/>
<asset:javascript src="util/tab-router.js"/>
<g:jsMessages code="page.unsaved.changes"/>
<feature:enabled name="notificationsEditorVue">
<asset:javascript src="static/pages/job/editor.js" defer="defer"/>
<asset:stylesheet src="static/css/pages/job/editor.css" />
<g:jsMessages code="
yes,
no,
scheduledExecution.property.notified.label.text,
scheduledExecution.property.notifyAvgDurationThreshold.label,
scheduledExecution.property.notifyAvgDurationThreshold.description,
to,
subject,
notification.email.description,
notification.email.subject.description,
notification.email.subject.helpLink,
attach.output.log,
attach.output.log.asFile,
attach.output.log.inline,
notification.webhook.field.title,
notification.webhook.field.description,
notify.url.format.label,
notify.url.format.xml,
notify.url.format.json,
"/>
<g:jsMessages codes="${[
'onsuccess',
'onfailure',
'onstart',
'onavgduration',
'onretryablefailure'
].collect{'notification.event.'+it}}"/>

<g:embedJSON id="jobNotificationsJSON"
data="${ [notifications:scheduledExecution.notifications?.collect{it.toNormalizedMap()}?:[],
notifyAvgDurationThreshold:scheduledExecution?.notifyAvgDurationThreshold,
]}"/>
</feature:enabled>
<g:javascript>

<feature:enabled name="notificationsEditorVue">
window._rundeck = Object.assign(window._rundeck || {}, {
data: {notificationData: loadJsonData('jobNotificationsJSON')}
})
</feature:enabled>
console.log("loaded data",window._rundeck)
var workflowEditor = new WorkflowEditor();
var confirm = new PageConfirm(message('page.unsaved.changes'));
_onJobEdit(confirm.setNeedsConfirm);
Expand Down
43 changes: 43 additions & 0 deletions rundeckapp/grails-app/views/scheduledExecution/edit.gsp
Expand Up @@ -30,7 +30,50 @@
<asset:javascript src="util/yellowfade.js"/>
<asset:javascript src="util/tab-router.js"/>
<g:jsMessages code="page.unsaved.changes"/>
<feature:enabled name="notificationsEditorVue">
<asset:javascript src="static/pages/job/editor.js" defer="defer"/>
<asset:stylesheet src="static/css/pages/job/editor.css" />
<g:jsMessages code="
yes,
no,
scheduledExecution.property.notified.label.text,
scheduledExecution.property.notifyAvgDurationThreshold.label,
scheduledExecution.property.notifyAvgDurationThreshold.description,
to,
subject,
notification.email.description,
notification.email.subject.description,
notification.email.subject.helpLink,
attach.output.log,
attach.output.log.asFile,
attach.output.log.inline,
notification.webhook.field.title,
notification.webhook.field.description,
notify.url.format.label,
notify.url.format.xml,
notify.url.format.json,
"/>
<g:jsMessages codes="${[
'onsuccess',
'onfailure',
'onstart',
'onavgduration',
'onretryablefailure'
].collect{'notification.event.'+it}}"/>

<g:embedJSON id="jobNotificationsJSON"
data="${ [notifications:scheduledExecution.notifications?.collect{it.toNormalizedMap()}?:[],
notifyAvgDurationThreshold:scheduledExecution?.notifyAvgDurationThreshold,
]}"/>
</feature:enabled>

<g:javascript>

<feature:enabled name="notificationsEditorVue">
window._rundeck = Object.assign(window._rundeck || {}, {
data: {notificationData: loadJsonData('jobNotificationsJSON')}
})
</feature:enabled>
var workflowEditor = new WorkflowEditor();
var confirm = new PageConfirm(message('page.unsaved.changes'));
_onJobEdit(confirm.setNeedsConfirm);
Expand Down