diff --git a/admin/app/Application.scala b/admin/app/Application.scala index b84ca05..6b0bf54 100644 --- a/admin/app/Application.scala +++ b/admin/app/Application.scala @@ -27,6 +27,7 @@ class PiezoAdminComponents(context: Context) extends BuiltInComponentsFromContex lazy val schedulerFactory: WorkerSchedulerFactory = new WorkerSchedulerFactory() lazy val jobFormHelper: JobFormHelper = wire[JobFormHelper] + lazy val monitoringTeams: MonitoringTeams = wire[MonitoringTeams] lazy val triggers: Triggers = wire[Triggers] lazy val jobs: Jobs = wire[Jobs] diff --git a/admin/app/com/lucidchart/piezo/admin/controllers/Jobs.scala b/admin/app/com/lucidchart/piezo/admin/controllers/Jobs.scala index d15b579..47bd9fc 100644 --- a/admin/app/com/lucidchart/piezo/admin/controllers/Jobs.scala +++ b/admin/app/com/lucidchart/piezo/admin/controllers/Jobs.scala @@ -14,6 +14,7 @@ import scala.jdk.CollectionConverters._ import scala.collection.mutable import scala.Some import scala.io.Source +import com.lucidchart.piezo.admin.models.MonitoringTeams trait ImportResult { val jobKey: Option[JobKey] @@ -35,14 +36,14 @@ trait ImportResult { case class ImportSuccess(val jobKey: Option[JobKey], val errorMessage: String = "", val success: Boolean=true) extends ImportResult case class ImportFailure(val jobKey: Option[JobKey], val errorMessage: String, val success: Boolean=false) extends ImportResult -class Jobs(schedulerFactory: WorkerSchedulerFactory, jobView: html.job, cc: ControllerComponents) extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport { +class Jobs(schedulerFactory: WorkerSchedulerFactory, jobView: html.job, cc: ControllerComponents, monitoringTeams: MonitoringTeams) extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport { val scheduler = logExceptions(schedulerFactory.getScheduler()) val properties = schedulerFactory.props val jobHistoryModel = logExceptions(new JobHistoryModel(properties)) val triggerMonitoringPriorityModel = logExceptions(new TriggerMonitoringModel(properties)) val jobFormHelper = new JobFormHelper() - val triggerFormHelper = new TriggerFormHelper(scheduler) + val triggerFormHelper = new TriggerFormHelper(scheduler, monitoringTeams) // Allow up to 1M private val maxFormSize = 1024 * 1024 diff --git a/admin/app/com/lucidchart/piezo/admin/controllers/TriggerFormHelper.scala b/admin/app/com/lucidchart/piezo/admin/controllers/TriggerFormHelper.scala index 0388e8a..33cb7d0 100644 --- a/admin/app/com/lucidchart/piezo/admin/controllers/TriggerFormHelper.scala +++ b/admin/app/com/lucidchart/piezo/admin/controllers/TriggerFormHelper.scala @@ -2,6 +2,7 @@ package com.lucidchart.piezo.admin.controllers import com.lucidchart.piezo.TriggerMonitoringPriority import com.lucidchart.piezo.TriggerMonitoringPriority.TriggerMonitoringPriority +import com.lucidchart.piezo.admin.models.MonitoringTeams import com.lucidchart.piezo.admin.utils.CronHelper import java.text.ParseException import org.quartz._ @@ -11,7 +12,7 @@ import play.api.data.format.Formats.parsing import play.api.data.format.Formatter import play.api.data.validation.{Constraint, Constraints, Invalid, Valid, ValidationError} -class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper { +class TriggerFormHelper(scheduler: Scheduler, monitoringTeams: MonitoringTeams) extends JobDataHelper { private def simpleScheduleFormApply(repeatCount: Int, repeatInterval: Int): SimpleScheduleBuilder = { SimpleScheduleBuilder @@ -161,6 +162,12 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper { fields => { scheduler.checkExists(fields._1.getJobKey) }, + ) + .verifying( + "Team is required if monitoring is on", + fields => { + !monitoringTeams.teamsDefined || fields._2 == TriggerMonitoringPriority.Off || fields._4.nonEmpty + } ), ) } diff --git a/admin/app/com/lucidchart/piezo/admin/controllers/Triggers.scala b/admin/app/com/lucidchart/piezo/admin/controllers/Triggers.scala index d8b4f5c..8d4e4b6 100644 --- a/admin/app/com/lucidchart/piezo/admin/controllers/Triggers.scala +++ b/admin/app/com/lucidchart/piezo/admin/controllers/Triggers.scala @@ -15,13 +15,14 @@ import scala.util.Try import play.api.Logging import play.api.i18n.I18nSupport -class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponents) extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport { +class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponents, monitoringTeams: MonitoringTeams) + extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport { val scheduler = logExceptions(schedulerFactory.getScheduler()) val properties = schedulerFactory.props val triggerHistoryModel = logExceptions(new TriggerHistoryModel(properties)) val triggerMonitoringPriorityModel = logExceptions(new TriggerMonitoringModel(properties)) - val triggerFormHelper = new TriggerFormHelper(scheduler) + val triggerFormHelper = new TriggerFormHelper(scheduler, monitoringTeams) def firesFirst(time: Date)(trigger1: Trigger, trigger2: Trigger): Boolean = { val time1 = trigger1.getFireTimeAfter(time) @@ -165,12 +166,12 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent Ok( com.lucidchart.piezo.admin.views.html.editTrigger( TriggerHelper.getTriggersByGroup(scheduler), + monitoringTeams.value, newTriggerForm, formNewAction, false, false ) - (request, implicitly) ) } } @@ -200,22 +201,24 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent Ok( com.lucidchart.piezo.admin.views.html.editTrigger( TriggerHelper.getTriggersByGroup(scheduler), + monitoringTeams.value, editTriggerForm, formNewAction, false, isTemplate - )(request, implicitly) + ) ) } else { Ok( com.lucidchart.piezo.admin.views.html.editTrigger( TriggerHelper.getTriggersByGroup(scheduler), + monitoringTeams.value, editTriggerForm, formEditAction(group, name), true, isTemplate - )(request, implicitly) + ) ) } } @@ -231,6 +234,7 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent BadRequest( com.lucidchart.piezo.admin.views.html.editTrigger( TriggerHelper.getTriggersByGroup(scheduler), + monitoringTeams.value, formWithErrors, formEditAction(group, name), true, @@ -258,6 +262,7 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent BadRequest( com.lucidchart.piezo.admin.views.html.editTrigger( TriggerHelper.getTriggersByGroup(scheduler), + monitoringTeams.value, formWithErrors, formNewAction, false, @@ -284,12 +289,13 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent Ok( com.lucidchart.piezo.admin.views.html.editTrigger( TriggerHelper.getTriggersByGroup(scheduler), + monitoringTeams.value, form, formNewAction, false, false, errorMessage = Some("Please provide unique group-name pair") - )(request, implicitly) + ) ) } } diff --git a/admin/app/com/lucidchart/piezo/admin/models/MonitoringTeams.scala b/admin/app/com/lucidchart/piezo/admin/models/MonitoringTeams.scala new file mode 100644 index 0000000..973c3c6 --- /dev/null +++ b/admin/app/com/lucidchart/piezo/admin/models/MonitoringTeams.scala @@ -0,0 +1,28 @@ +package com.lucidchart.piezo.admin.models + +import play.api.Configuration +import java.nio.file.Files +import play.api.libs.json.Json +import scala.util.Try +import java.io.File +import java.io.FileInputStream +import play.api.libs.json.JsArray + +class MonitoringTeams(configuration: Configuration) { + private val path = configuration.getOptional[String]("com.lucidchart.piezo.admin.monitoringTeams.path") + + val value = path.flatMap(p => + Try( + Json.parse(new FileInputStream(p)) + .as[JsArray] + .value + .map(entry => (entry \ "name").as[String]) + .toSeq + ).toOption + ).getOrElse(Seq.empty) + + def teamsDefined: Boolean = value.nonEmpty +} +object MonitoringTeams { + def empty: MonitoringTeams = new MonitoringTeams(Configuration.empty) +} diff --git a/admin/app/com/lucidchart/piezo/admin/views/editTrigger.scala.html b/admin/app/com/lucidchart/piezo/admin/views/editTrigger.scala.html index be423e8..e3e519f 100644 --- a/admin/app/com/lucidchart/piezo/admin/views/editTrigger.scala.html +++ b/admin/app/com/lucidchart/piezo/admin/views/editTrigger.scala.html @@ -1,15 +1,16 @@ @( triggersByGroup: scala.collection.mutable.Buffer[(String, scala.collection.immutable.List[org.quartz.TriggerKey])], +monitoringTeams: Seq[String], triggerForm: Form[(org.quartz.Trigger, com.lucidchart.piezo.TriggerMonitoringPriority.Value, Int, Option[String])], formAction: play.api.mvc.Call, existing: Boolean, isTemplate: Boolean, errorMessage: Option[String] = None, -scripts: List[String] = List[String]("js/jobData.js", "js/typeAhead.js") +scripts: List[String] = List[String]("js/jobData.js", "js/typeAhead.js", "js/triggerMonitoring.js") )( implicit request: play.api.mvc.Request[AnyContent], -messagesProvider: play.api.i18n.MessagesProvider +messagesProvider: play.api.i18n.MessagesProvider, ) @import com.lucidchart.piezo.TriggerMonitoringPriority @@ -67,12 +68,18 @@

@triggerForm.errors.filter(_.key == "").map(_.message).m } } @helper.select(triggerForm("triggerMonitoringPriority"), TriggerMonitoringPriority.values.map(tp => tp.name -> tp.name), Symbol("_label") -> "Monitoring Priority", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("class") -> "form-control", Symbol("value") -> triggerForm.data.get("triggerMonitoringPriority").getOrElse(TriggerMonitoringPriority.Low), Symbol("placeholder") -> TriggerMonitoringPriority.Low) - @helper.input(triggerForm("triggerMaxErrorTime"), Symbol("_label") -> "Monitoring - Max Seconds Between Successes", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMaxErrorTime").getOrElse(300)) { (id, name, value, args) => - - } - @helper.input(triggerForm("triggerMonitoringTeam"), Symbol("_label") -> "Monitoring team", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMonitoringTeam").getOrElse(None)) { (id, name, value, args) => - - } +
+ @helper.input(triggerForm("triggerMaxErrorTime"), Symbol("_label") -> "Monitoring - Max Seconds Between Successes", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMaxErrorTime").getOrElse(300)) { (id, name, value, args) => + + } + @if(monitoringTeams.nonEmpty) { + @helper.select(triggerForm("triggerMonitoringTeam"), monitoringTeams.map(mt => mt -> mt), Symbol("_default") -> "Select team", Symbol("_label") -> "Monitoring team", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("class") -> "form-control", Symbol("value") -> triggerForm.data.get("triggerMonitoringTeam").getOrElse("")) + } else { + @helper.input(triggerForm("triggerMonitoringTeam"), Symbol("_label") -> "Monitoring team", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMonitoringTeam").getOrElse(None)) { (id, name, value, args) => + + } + } +
@helper.input(triggerForm("description"), Symbol("_label") -> "Description", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-10", Symbol("placeholder") -> "Description", Symbol("value")-> triggerForm.data.get("description").getOrElse("")) { (id, name, value, args) => diff --git a/admin/conf/application.conf b/admin/conf/application.conf index 962802b..7907dbd 100644 --- a/admin/conf/application.conf +++ b/admin/conf/application.conf @@ -46,3 +46,14 @@ com.lucidchart.piezo.heartbeatFile="/tmp/piezo/workerHeartbeatFile" com.lucidchart.piezo.admin.production=false healthCheck.worker.minutesBetween=5 play.application.loader=com.lucidchart.piezo.admin.PiezoAdminApplicationLoader + +# Monitoring teams +# ~~~~~ +# Path to a JSON file that fills the "Monitoring Team" dropdown on editTrigger +# in the admin UI with a predefined set of team names. File format: +# [ +# {"name": "team1"}, +# {"name": "team2"} +# ] +# If this is left blank, monitoring team will be a freeform input. +# com.lucidchart.piezo.admin.monitoringTeams.path = "/etc/piezo/teams.json" diff --git a/admin/public/js/triggerMonitoring.js b/admin/public/js/triggerMonitoring.js new file mode 100644 index 0000000..f829b64 --- /dev/null +++ b/admin/public/js/triggerMonitoring.js @@ -0,0 +1,14 @@ +(function () { + var setMonitoringFieldVisibility = function() { + var priority = $('#triggerMonitoringPriority option:selected').val(); + if (priority === 'Off') { + $('#triggerMonitoringDetails').hide(); + } else { + $('#triggerMonitoringDetails').show(); + } + }; + + $('#triggerMonitoringPriority').on('change', setMonitoringFieldVisibility); + + setMonitoringFieldVisibility(); +})(); diff --git a/admin/test/com/lucidchart/piezo/admin/controllers/JobsService.scala b/admin/test/com/lucidchart/piezo/admin/controllers/JobsService.scala index e283d33..2dfbc9d 100644 --- a/admin/test/com/lucidchart/piezo/admin/controllers/JobsService.scala +++ b/admin/test/com/lucidchart/piezo/admin/controllers/JobsService.scala @@ -1,7 +1,6 @@ package com.lucidchart.piezo.admin.controllers import org.specs2.mutable._ - import play.api.test._ import play.api.test.Helpers._ import com.lucidchart.piezo.jobs.monitoring.HeartBeat @@ -14,8 +13,8 @@ import com.lucidchart.piezo.util.DummyClassGenerator import play.api.mvc.{Result, AnyContentAsEmpty} import java.util.Properties import play.api.Configuration - import scala.concurrent.Future +import com.lucidchart.piezo.admin.models.MonitoringTeams /** * Add your spec here. @@ -39,7 +38,7 @@ class JobsService extends Specification { properties.load(propertiesStream) schedulerFactory.initialize(properties) - val jobsController = new Jobs(schedulerFactory, jobView, Helpers.stubControllerComponents()) + val jobsController = new Jobs(schedulerFactory, jobView, Helpers.stubControllerComponents(), MonitoringTeams.empty) val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/jobs/missinggroup/missingname") val missingJob: Future[Result] = jobsController.getJob("missinggroup", "missingname")(request) @@ -57,8 +56,7 @@ class JobsService extends Specification { val scheduler = schedulerFactory.getScheduler() createJob(scheduler) - - val jobsController = new Jobs(schedulerFactory, jobView, Helpers.stubControllerComponents()) + val jobsController = new Jobs(schedulerFactory, jobView, Helpers.stubControllerComponents(), MonitoringTeams.empty) val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/jobs/" + jobGroup + "/" + jobName) val validJob: Future[Result] = jobsController.getJob(jobGroup, jobName)(request) diff --git a/admin/test/com/lucidchart/piezo/admin/controllers/TriggersService.scala b/admin/test/com/lucidchart/piezo/admin/controllers/TriggersService.scala index 0c42bb7..4c3030d 100644 --- a/admin/test/com/lucidchart/piezo/admin/controllers/TriggersService.scala +++ b/admin/test/com/lucidchart/piezo/admin/controllers/TriggersService.scala @@ -1,7 +1,6 @@ package com.lucidchart.piezo.admin.controllers import org.specs2.mutable._ - import play.api.test._ import play.api.test.Helpers._ import com.lucidchart.piezo.WorkerSchedulerFactory @@ -9,49 +8,49 @@ import TestUtil._ import java.util.Properties import play.api.mvc.{AnyContentAsEmpty, Result} import scala.concurrent.Future +import com.lucidchart.piezo.admin.models.MonitoringTeams /** - * Add your spec here. - * You can mock out a whole application including requests, plugins etc. - * For more information, consult the wiki. - */ + * Add your spec here. You can mock out a whole application including requests, plugins etc. For more information, + * consult the wiki. + */ class TriggersService extends Specification { - "Triggers" should { - - "send 404 on a non-existent trigger request" in { - val schedulerFactory: WorkerSchedulerFactory = new WorkerSchedulerFactory() - - val propertiesStream = getClass().getResourceAsStream("/quartz_test.properties") - val properties = new Properties - properties.load(propertiesStream) - schedulerFactory.initialize(properties) - - val triggersController = new Triggers(schedulerFactory, Helpers.stubControllerComponents()) - val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/triggers/missinggroup/missingname") - val missingTrigger: Future[Result] = triggersController.getTrigger("missinggroup", "missingname")(request) - - status(missingTrigger) must equalTo(NOT_FOUND) - contentType(missingTrigger) must beSome.which(_ == "text/html") - contentAsString(missingTrigger) must contain ("Trigger missinggroup missingname not found") - } - - "send valid trigger details" in { - val schedulerFactory: WorkerSchedulerFactory = new WorkerSchedulerFactory() - val propertiesStream = getClass().getResourceAsStream("/quartz_test.properties") - val properties = new Properties - properties.load(propertiesStream) - schedulerFactory.initialize(properties) - val scheduler = schedulerFactory.getScheduler() - createJob(scheduler) - - val triggersController = new Triggers(schedulerFactory, Helpers.stubControllerComponents()) - val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/triggers/" + jobGroup + "/" + jobName) - val validTrigger: Future[Result] = triggersController.getTrigger(triggerGroup, triggerName)(request) - - status(validTrigger) must equalTo(OK) - contentType(validTrigger) must beSome.which(_ == "text/html") - contentAsString(validTrigger) must contain (triggerGroup) - contentAsString(validTrigger) must contain (triggerName) - } - } - } + "Triggers" should { + + "send 404 on a non-existent trigger request" in { + val schedulerFactory: WorkerSchedulerFactory = new WorkerSchedulerFactory() + + val propertiesStream = getClass().getResourceAsStream("/quartz_test.properties") + val properties = new Properties + properties.load(propertiesStream) + schedulerFactory.initialize(properties) + + val triggersController = new Triggers(schedulerFactory, Helpers.stubControllerComponents(), MonitoringTeams.empty) + val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/triggers/missinggroup/missingname") + val missingTrigger: Future[Result] = triggersController.getTrigger("missinggroup", "missingname")(request) + + status(missingTrigger) must equalTo(NOT_FOUND) + contentType(missingTrigger) must beSome.which(_ == "text/html") + contentAsString(missingTrigger) must contain("Trigger missinggroup missingname not found") + } + + "send valid trigger details" in { + val schedulerFactory: WorkerSchedulerFactory = new WorkerSchedulerFactory() + val propertiesStream = getClass().getResourceAsStream("/quartz_test.properties") + val properties = new Properties + properties.load(propertiesStream) + schedulerFactory.initialize(properties) + val scheduler = schedulerFactory.getScheduler() + createJob(scheduler) + + val triggersController = new Triggers(schedulerFactory, Helpers.stubControllerComponents(), MonitoringTeams.empty) + val request: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(GET, "/triggers/" + jobGroup + "/" + jobName) + val validTrigger: Future[Result] = triggersController.getTrigger(triggerGroup, triggerName)(request) + + status(validTrigger) must equalTo(OK) + contentType(validTrigger) must beSome.which(_ == "text/html") + contentAsString(validTrigger) must contain(triggerGroup) + contentAsString(validTrigger) must contain(triggerName) + } + } +}