Skip to content

Commit

Permalink
Optionally change trigger monitoring team into a dropdown of teams en…
Browse files Browse the repository at this point in the history
…umerated in a local file
  • Loading branch information
Tanner Jorgensen committed Jan 15, 2025
1 parent ee6d51d commit 6bd54ea
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 66 deletions.
1 change: 1 addition & 0 deletions admin/app/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
5 changes: 3 additions & 2 deletions admin/app/com/lucidchart/piezo/admin/controllers/Jobs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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
Expand Down Expand Up @@ -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
}
),
)
}
Expand Down
18 changes: 12 additions & 6 deletions admin/app/com/lucidchart/piezo/admin/controllers/Triggers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
)
}
}
Expand Down Expand Up @@ -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)
)
)
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)
)
)
}
}
Expand Down
28 changes: 28 additions & 0 deletions admin/app/com/lucidchart/piezo/admin/models/MonitoringTeams.scala
Original file line number Diff line number Diff line change
@@ -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)
}
23 changes: 15 additions & 8 deletions admin/app/com/lucidchart/piezo/admin/views/editTrigger.scala.html
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -67,12 +68,18 @@ <h4 class="text-danger">@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) =>
<input type="number" class="form-control form-inline-control " name="@name" id="@id" @toHtmlArgs(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) =>
<input type="text" class="form-control form-inline-control" name="@name" id="@id" @toHtmlArgs(args)>
}
<div id="triggerMonitoringDetails">
@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) =>
<input type="number" class="form-control form-inline-control " name="@name" id="@id" @toHtmlArgs(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) =>
<input type="text" class="form-control form-inline-control" name="@name" id="@id" @toHtmlArgs(args)>
}
}
</div>

@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) =>
<input type="text" class="form-control form-inline-control " name="@name" id="@id" @toHtmlArgs(args)>
Expand Down
11 changes: 11 additions & 0 deletions admin/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
14 changes: 14 additions & 0 deletions admin/public/js/triggerMonitoring.js
Original file line number Diff line number Diff line change
@@ -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();
})();
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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)

Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
package com.lucidchart.piezo.admin.controllers

import org.specs2.mutable._

import play.api.test._
import play.api.test.Helpers._
import com.lucidchart.piezo.WorkerSchedulerFactory
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)
}
}
}

0 comments on commit 6bd54ea

Please sign in to comment.