Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
  • 7 commits
  • 25 files changed
  • 0 commit comments
  • 3 contributors
Commits on Mar 19, 2013
@niagara-bot niagara-bot [RJA-1957][feature] dashboard b81593e
@niagara-bot niagara-bot [RJA-2008][fix] removing additional render request 85e812e
ddurnev Merge pull request #888 from bugzmanov/jobs
[RJA-1957][feature] dashboard
cdb1f1f
Commits on Mar 20, 2013
@niagara-bot niagara-bot [RJA-1898][feature] additional hypermedia links 872fd5d
@rsvato Merge pull request #889 from bugzmanov/jobs
[RJA-1898][feature] additional hypermedia links
ee416b1
ddurnev Merge pull request #890 from rsvato/master
[DOC-XXX][fix] - DB templates: changed properties to genesis.system prefix
5c83d28
Commits on Mar 21, 2013
@rsvato [RJA-1911] [ feature] - Expand failed steps dc33ee3
Showing with 552 additions and 142 deletions.
  1. +6 −0 api/src/main/scala/com/griddynamics/genesis/api/Model.scala
  2. +8 −2 backend/src/main/scala/com/griddynamics/genesis/configuration/SchedulerContext.scala
  3. +23 −0 backend/src/main/scala/com/griddynamics/genesis/repository/impl/FailedJobRepositoryImpl.scala
  4. +31 −16 backend/src/main/scala/com/griddynamics/genesis/scheduler/EnvironmentJobServiceImpl.scala
  5. +26 −5 backend/src/main/scala/com/griddynamics/genesis/scheduler/ExecutedJobsServiceImpl.scala
  6. +30 −13 backend/src/main/scala/com/griddynamics/genesis/scheduler/Executions.scala
  7. +36 −11 backend/src/main/scala/com/griddynamics/genesis/scheduler/SchedulingService.scala
  8. +1 −1  frontend/src/main/resources/WEB-INF/spring/frontend-config.xml
  9. +2 −0  frontend/src/main/resources/WEB-INF/spring/security-config.xml
  10. +5 −1 frontend/src/main/scala/com/griddynamics/genesis/rest/RootController.scala
  11. +40 −9 .../scala/com/griddynamics/genesis/rest/{EnvironmentJobsController.scala → ScheduledJobsController.scala}
  12. +5 −2 internal-api/src/main/scala/com/griddynamics/genesis/service/ExecutedJobsService.scala
  13. +22 −0 test-automation/cucumber/templates/Simple.genesis
  14. +1 −5 ui/src/main/resources/genesis/app/app.js
  15. +7 −0 ui/src/main/resources/genesis/app/modules/breadcrumbs.js
  16. +111 −0 ui/src/main/resources/genesis/app/modules/dashboard/jobs.coffee
  17. +1 −3 ui/src/main/resources/genesis/app/modules/env_details/env_details.js
  18. +10 −1 ui/src/main/resources/genesis/app/modules/env_details/history.js
  19. +8 −1 ui/src/main/resources/genesis/app/router.js
  20. +29 −0 ui/src/main/resources/genesis/app/templates/dashboard/jobs.html
  21. +17 −0 ui/src/main/resources/genesis/app/templates/dashboard/project_jobs.html
  22. +2 −2 ui/src/main/resources/genesis/app/templates/env_details.html
  23. +51 −2 ui/src/main/resources/genesis/assets/css/style.css
  24. BIN  ui/src/main/resources/genesis/assets/img/calendar2.png
  25. +80 −68 ui/src/main/resources/genesis/index.html
View
6 api/src/main/scala/com/griddynamics/genesis/api/Model.scala
@@ -328,6 +328,12 @@ case class ScheduledJobDetails (
failureDescription: Option[String]
)
+case class ScheduledJobStat (
+ projectId: Int,
+ scheduledJobs: Long,
+ failedJobs: Long
+)
+
case class JobStats(runningJobs: Int, totalJobs: Int)
case class Link(href: String, rel: String, `type`: Option[String], methods: Array[String] = Array()) {
View
10 backend/src/main/scala/com/griddynamics/genesis/configuration/SchedulerContext.scala
@@ -65,7 +65,9 @@ class SchedulerContext extends Logging {
}
WorkflowEventsBus.subscribe {
- case EnvDestroyed(projectId, envId) => envJobService.removeAllScheduledJobs(projectId, envId)
+ case EnvDestroyed(projectId, envId) =>
+ envJobService.removeAllScheduledJobs(projectId, envId)
+ executedJobsService.removeFailedJobsRecords(projectId, envId)
}
}
@@ -90,7 +92,7 @@ class SchedulerContext extends Logging {
@Bean def jobListener = new FailedJobsListener(failedJobRepository)
- @Bean def executedJobsService: ExecutedJobsService = new ExecutedJobsServiceImpl(failedJobRepository)
+ @Bean def executedJobsService: ExecutedJobsService = new ExecutedJobsServiceImpl(failedJobRepository, envJobService)
@Bean def scheduler: SchedulerFactoryBean = {
val factory = new SchedulerFactoryBean()
@@ -146,5 +148,9 @@ class SchedulerStubContext {
def removeJob(projectId: Int, envId: Int, jobId: String) {}
def scheduleExecution(projectId: Int, envId: Int, workflow: String, parameters: Map[String, String], date: Date, requestedBy: String) = Success(date)
+
+ def listScheduledJobs(projectId: Int) = Seq()
+
+ def scheduledJobsStat = Map()
}
}
View
23 ...end/src/main/scala/com/griddynamics/genesis/repository/impl/FailedJobRepositoryImpl.scala
@@ -33,6 +33,9 @@ trait FailedJobRepository {
def list(envId: Int): Iterable[api.ScheduledJobDetails]
def logFailure(job: api.ScheduledJobDetails): api.ScheduledJobDetails
def delete(envId: GenesisEntity.Id, id: GenesisEntity.Id): Int
+ def failedJobStats: Seq[(Int, Long)]
+ def listByProject(projectId: Int):Iterable[api.ScheduledJobDetails]
+ def deleteRecords(projectId: GenesisEntity.Id, envId: GenesisEntity.Id): Int
}
class FailedJobRepositoryImpl extends AbstractGenericRepository[model.FailedJobDetails, api.ScheduledJobDetails](GS.failedJobDetails) with FailedJobRepository {
@@ -54,7 +57,27 @@ class FailedJobRepositoryImpl extends AbstractGenericRepository[model.FailedJobD
def delete(envId: GenesisEntity.Id, id: GenesisEntity.Id) = table.deleteWhere(jd => jd.id === id and jd.envId === envId)
@Transactional(readOnly = true)
+ def listByProject(projectId: Int):Iterable[api.ScheduledJobDetails] = from(table) {t =>
+ where (t.projectId === projectId) select (t)
+ }.toList.map(convert)
+
+ @Transactional(readOnly = true)
+ def failedJobStats = {
+ val group = from(table)( t =>
+ groupBy(t.projectId)
+ compute(countDistinct(t.id))
+ ).toList
+
+ group.map(g=> (g.key, g.measures)).toSeq
+ }
+
+ @Transactional(readOnly = true)
def list(envId: Int) = from(table) { t =>
where(t.envId === envId) select (t)
}.toList.map(convert)
+
+ @Transactional
+ def deleteRecords(projectId: GenesisEntity.Id, envId: GenesisEntity.Id) = {
+ table.deleteWhere(jd => jd.envId === envId and jd.projectId === projectId )
+ }
}
View
47 backend/src/main/scala/com/griddynamics/genesis/scheduler/EnvironmentJobServiceImpl.scala
@@ -38,8 +38,10 @@ trait EnvironmentJobService {
def removeScheduledDestruction(projectId: Int, envId: Int)
def removeAllScheduledJobs(projectId: Int, envId: Int)
def listScheduledJobs(projectId: Int, envId: Int): Seq[ScheduledJobDetails]
+ def listScheduledJobs(projectId: Int): Seq[ScheduledJobDetails]
def removeJob(projectId: Int, envId: Int, jobId: String)
def scheduleExecution(projectId: Int, envId: Int, workflow: String, parameters: Map[String, String], date: Date, requestedBy: String): ExtendedResult[Date]
+ def scheduledJobsStat: Map[Int, Long]
}
class EnvironmentJobServiceImpl(scheduler: SchedulingService,
@@ -87,7 +89,7 @@ class EnvironmentJobServiceImpl(scheduler: SchedulingService,
@Transactional(readOnly = true)
def executionDate(env: Environment, workflow: String): Option[Date] = {
- val id = new WorkflowExecutionId(env.id, workflow)
+ val id = new WorkflowExecutionId(env.projectId, env.id, workflow)
scheduler.getScheduledDate(id)
}
@@ -106,7 +108,7 @@ class EnvironmentJobServiceImpl(scheduler: SchedulingService,
def removeScheduledDestruction(projectId: Int, envId: Int) {
for ( env <- storeService.findEnv(envId, projectId);
template <- templateService.findTemplate(env) ) {
- val jobs = Seq(new DestructionCheckId(envId), new ExpireNotificationId(envId), new WorkflowExecutionId(envId, template.destroyWorkflow.name))
+ val jobs = Seq(new DestructionCheckId(projectId, envId), new ExpireNotificationId(projectId, envId), new WorkflowExecutionId(projectId, envId, template.destroyWorkflow.name))
scheduler.removeScheduledJobs(jobs)
}
}
@@ -127,7 +129,7 @@ class EnvironmentJobServiceImpl(scheduler: SchedulingService,
scheduleDestructionJobs(env, template, requestedBy, date)
Success(date)
} else {
- scheduler.removeScheduledJob(new WorkflowExecutionId(envId, workflow))
+ scheduler.removeScheduledJob(new WorkflowExecutionId(projectId, envId, workflow))
scheduleWorkflow(template, workflow, parameters, configuration, env, requestedBy, date)
}
}
@@ -151,13 +153,13 @@ class EnvironmentJobServiceImpl(scheduler: SchedulingService,
@Transactional
def removeJob(projectId: Int, envId: Int, jobId: String) {
- val execution = scheduler.getJob(envId, jobId)
+ val execution = scheduler.getJob(projectId, envId, jobId)
execution match {
case e: WorkflowExecution =>
if(isDestroyWorkflow(envId, projectId, e.workflow)) {
removeScheduledDestruction(projectId, envId)
} else {
- scheduler.removeScheduledJob(new WorkflowExecutionId(envId, e.workflow))
+ scheduler.removeScheduledJob(new WorkflowExecutionId(projectId, envId, e.workflow))
}
case _ => //do nothing cause only workflow executions are propagated to upper layers
}
@@ -172,20 +174,26 @@ class EnvironmentJobServiceImpl(scheduler: SchedulingService,
}
val s = scheduler.listJobs(projectId, envId)
- val details = s.collect { case (date, execution: WorkflowExecution) if date != null => new ScheduledJobDetails(
- id = execution.id,
- projectId = projectId,
- envId = env.id,
- date = date.getTime,
- workflow = execution.workflow,
- variables = execution.variables,
- scheduledBy = execution.requestedBy,
- failureDescription = None
- )}
- details
+ convert(s, projectId)
}
+ private def convert(s: Seq[(Date, Execution)], projectId: Int): Seq[ScheduledJobDetails] = {
+ val details = s.collect {
+ case (date, execution: WorkflowExecution) if date != null => new ScheduledJobDetails(
+ id = execution.id,
+ projectId = projectId,
+ envId = execution.envId,
+ date = date.getTime,
+ workflow = execution.workflow,
+ variables = execution.variables,
+ scheduledBy = execution.requestedBy,
+ failureDescription = None
+ )
+ }
+ details
+ }
+
private def isDestroyWorkflow(envId: Int, projectId: Int, workflowName: String): Boolean = {
val definition = for {
env <- storeService.findEnv(envId, projectId)
@@ -195,4 +203,11 @@ class EnvironmentJobServiceImpl(scheduler: SchedulingService,
definition.map(_.name == workflowName).getOrElse(false)
}
+ @Transactional(readOnly = true)
+ def listScheduledJobs(projectId: Int): Seq[ScheduledJobDetails] = {
+ convert(scheduler.listJobs(projectId), projectId)
+ }
+
+ @Transactional(readOnly = true)
+ def scheduledJobsStat: Map[Int, Long] = scheduler.jobsPerProjectStat
}
View
31 backend/src/main/scala/com/griddynamics/genesis/scheduler/ExecutedJobsServiceImpl.scala
@@ -23,19 +23,40 @@
import com.griddynamics.genesis.service.ExecutedJobsService
import com.griddynamics.genesis.repository.impl.FailedJobRepository
-import com.griddynamics.genesis.api.{Success, Failure}
+import com.griddynamics.genesis.api.{ExtendedResult, ScheduledJobDetails, ScheduledJobStat, Success, Failure}
import org.springframework.transaction.annotation.Transactional
-class ExecutedJobsServiceImpl(repo: FailedJobRepository) extends ExecutedJobsService {
+class ExecutedJobsServiceImpl(failedJobsRepo: FailedJobRepository, envJobService: EnvironmentJobService) extends ExecutedJobsService {
@Transactional(readOnly = true)
- def listFailedJobs(envId: Int) = repo.list(envId)
+ def listFailedJobs(envId: Int) = failedJobsRepo.list(envId)
@Transactional
- def removeJobRecord(envId: Int, jobId: Int) = {
- repo.delete(envId, jobId) match {
+ def removeFailedJobRecord(envId: Int, jobId: Int) = {
+ failedJobsRepo.delete(envId, jobId) match {
case 0 => Failure(compoundServiceErrors = Seq(s"Failed to find jobId $jobId related to env id = $envId"), isNotFound = true)
case _ => Success(jobId)
}
}
+
+ @Transactional
+ def removeFailedJobsRecords(projectId: Int, envId: Int): ExtendedResult[Int] = {
+ Success(failedJobsRepo.deleteRecords(projectId, envId))
+ }
+
+ @Transactional(readOnly = true)
+ def jobsStat: Seq[ScheduledJobStat] = {
+ val scheduled = envJobService.scheduledJobsStat
+ val failed = failedJobsRepo.failedJobStats.toMap
+ (failed.map { case (projectId, failedCount) =>
+ new ScheduledJobStat(projectId = projectId, scheduledJobs = scheduled.getOrElse(projectId, 0), failedJobs = failedCount)
+ } ++ (scheduled -- failed.keys).map { case (projectId, requested) =>
+ new ScheduledJobStat(projectId = projectId, scheduledJobs = requested, failedJobs = 0)
+ }).toSeq
+ }
+
+ @Transactional(readOnly = true)
+ def listProjectJobs(projectId: Int): (Seq[ScheduledJobDetails], Seq[ScheduledJobDetails]) = {
+ (failedJobsRepo.listByProject(projectId).toSeq, envJobService.listScheduledJobs(projectId))
+ }
}
View
43 backend/src/main/scala/com/griddynamics/genesis/scheduler/Executions.scala
@@ -27,17 +27,25 @@ import java.util.Date
import com.griddynamics.genesis.model.VariablesField
object GroupIdentity {
- def forEnv(envId: Int) = "execution-group-for-env-" + envId
+ def forEnv(projectId: Int, envId: Int) = s"${forProjectPrefix(projectId)}-env-" + envId
- def workflowForEnv(envId: Int, workflow: String) = s"execution-group-for-env-$envId-$workflow"
+ def projectId(key: TriggerKey) = {
+ val pat = s"$basePrefix-group-for-project-(\\d+).*".r
+ val pat(projectId) = key.getGroup
+ projectId.toInt
+ }
+
+ def basePrefix = "execution"
+ def forProjectPrefix(projectId: Int) = s"$basePrefix-group-for-project-$projectId"
}
sealed trait ExecutionId {
+ def projectId: Int
def envId: Int
def id: String
- final def triggerKey: TriggerKey = new TriggerKey(id, GroupIdentity.forEnv(envId))
- final def jobKey: JobKey = new JobKey(id, GroupIdentity.forEnv(envId))
+ final def triggerKey: TriggerKey = new TriggerKey(id, GroupIdentity.forEnv(projectId, envId))
+ final def jobKey: JobKey = new JobKey(id, GroupIdentity.forEnv(projectId, envId))
}
object Execution {
@@ -53,6 +61,10 @@ object Execution {
throw new IllegalArgumentException(s"Job class ${details.getJobClass} is not supported")
}
}
+
+ def isWorkflowExecution(key: TriggerKey) = {
+ key.getName.matches("execution-.+-env-.+")
+ }
}
sealed trait Execution extends ExecutionId {
@@ -61,17 +73,22 @@ sealed trait Execution extends ExecutionId {
def jobClass: Class[_ <: Job]
}
-sealed class WorkflowExecutionId(val envId: Int, val workflow: String) extends ExecutionId {
+sealed class WorkflowExecutionId(val projectId: Int, val envId: Int, val workflow: String) extends ExecutionId {
def id = s"execution-$workflow-env-$envId"
}
+object WorkflowExecutionId {
+
+
+}
+
case class WorkflowExecution(
override val workflow: String,
override val envId: Int,
- projectId: Int,
+ override val projectId: Int,
requestedBy: String,
variables: Map[String, String])
- extends WorkflowExecutionId(envId, workflow)
+ extends WorkflowExecutionId(projectId, envId, workflow)
with Execution {
def this(jobDataMap: JobDataMap) = this(
@@ -99,11 +116,11 @@ case class WorkflowExecution(
// val id = "destruction-env-" + envId
//}
-sealed class ExpireNotificationId(val envId: Int) extends ExecutionId {
+sealed class ExpireNotificationId(val projectId: Int, val envId: Int) extends ExecutionId {
val id = s"env-expire-notification-$envId"
}
-case class ExpireNotification(override val envId: Int, projectId: Int, destroyDate: Date) extends ExpireNotificationId(envId) with Execution {
+case class ExpireNotification(override val envId: Int, override val projectId: Int, destroyDate: Date) extends ExpireNotificationId(projectId, envId) with Execution {
def this(jobDataMap: JobDataMap) = this(jobDataMap.getInt("envId"), jobDataMap.getInt("projectId"), new Date(jobDataMap.getLong("destroyDate")))
@@ -118,12 +135,12 @@ case class ExpireNotification(override val envId: Int, projectId: Int, destroyDa
def jobClass = classOf[NotificationJob]
}
-sealed class DestructionCheckId(val envId: Int) extends ExecutionId {
+sealed class DestructionCheckId(val projectId: Int, val envId: Int) extends ExecutionId {
def id = s"env-destruction-check-$envId"
}
-case class DestructionCheck(override val envId: Int, projectId: Int, destructionTrigger: String)
- extends DestructionCheckId(envId) with Execution {
+case class DestructionCheck(override val envId: Int, override val projectId: Int, destructionTrigger: String)
+ extends DestructionCheckId(projectId, envId) with Execution {
def this(jobDataMap: JobDataMap) = this(jobDataMap.getInt("envId"), jobDataMap.getInt("projectId"), jobDataMap.getString("destructionTriggerName"))
@@ -135,7 +152,7 @@ case class DestructionCheck(override val envId: Int, projectId: Int, destruction
data
}
- def destructionTriggerKey = new TriggerKey(destructionTrigger, GroupIdentity.forEnv(envId))
+ def destructionTriggerKey = new TriggerKey(destructionTrigger, GroupIdentity.forEnv(projectId, envId))
def jobClass = classOf[DestructionStatusCheckJob]
}
View
47 backend/src/main/scala/com/griddynamics/genesis/scheduler/SchedulingService.scala
@@ -2,7 +2,7 @@ package com.griddynamics.genesis.scheduler
import java.util
import java.util.Date
-import org.quartz.impl.matchers.GroupMatcher
+import org.quartz.impl.matchers.{EverythingMatcher, GroupMatcher}
import org.quartz.{TriggerKey, JobKey, JobBuilder, TriggerBuilder, Scheduler}
import org.springframework.transaction.annotation.Transactional
@@ -11,10 +11,12 @@ trait SchedulingService {
def removeScheduledJobs(executions: Seq[ExecutionId])
def removeScheduledJob(execution: ExecutionId)
def listJobs(projectId: Int, envId: Int): Seq[(Date, Execution)]
+ def listJobs(projectId: Int): Seq[(Date, Execution)]
def schedule(execution: Execution, date: Date)
def getScheduledDate(execution: ExecutionId): Option[Date]
def reschedule(execution: ExecutionId, newDate: Date): Option[Date]
- def getJob(envId: Int, jobId: String): Execution
+ def getJob(projectId: Int, envId: Int, jobId: String): Execution
+ def jobsPerProjectStat: Map[Int, Long]
}
class SchedulingServiceImpl(scheduler: Scheduler) extends SchedulingService {
@@ -48,7 +50,7 @@ class SchedulingServiceImpl(scheduler: Scheduler) extends SchedulingService {
@Transactional
def removeAllScheduledJobs(projectId: Int, envId: Int) {
- val keys = scheduler.getJobKeys(GroupIdentity.forEnv(envId))
+ val keys = scheduler.getJobKeys(GroupIdentity.forEnv(projectId, envId))
scheduler.deleteJobs(new util.ArrayList[JobKey](keys))
}
@@ -63,23 +65,46 @@ class SchedulingServiceImpl(scheduler: Scheduler) extends SchedulingService {
scheduler.deleteJobs(executions.map(_.jobKey))
}
- @Transactional
+ @Transactional(readOnly = true)
def listJobs(projectId: Int, envId: Int): Seq[(Date, Execution)] = {
import scala.collection.JavaConversions._
- val triggerKeys = scheduler.getTriggerKeys(GroupIdentity.forEnv(envId))
+ val triggerKeys = scheduler.getTriggerKeys(GroupIdentity.forEnv(projectId, envId))
+ loadByTriggerKeys(triggerKeys.toSet)
+ }
+
+ private def loadByTriggerKeys(triggerKeys: Set[TriggerKey]): Seq[(Date, Execution)] = {
triggerKeys.map { k =>
- val trigger = scheduler.getTrigger(k)
- val jobDetail = scheduler.getJobDetail(trigger.getJobKey)
- (trigger.getNextFireTime, Execution(jobDetail))
+ val trigger = scheduler.getTrigger(k)
+ val jobDetail = scheduler.getJobDetail(trigger.getJobKey)
+ (trigger.getNextFireTime, Execution(jobDetail))
}.toSeq
}
- @Transactional
- def getJob(envId: Int, jobId: String): Execution = {
- val jobKey = new JobKey(jobId, GroupIdentity.forEnv(envId))
+ @Transactional(readOnly = true)
+ def getJob(projectId: Int, envId: Int, jobId: String): Execution = {
+ val jobKey = new JobKey(jobId, GroupIdentity.forEnv(projectId, envId))
Execution(scheduler.getJobDetail(jobKey))
}
+ @Transactional(readOnly = true)
+ def listJobs(projectId: Int): Seq[(Date, Execution)] = {
+ import scala.collection.JavaConversions._
+ val keys = scheduler.getTriggerKeys(GroupMatcher.groupStartsWith(GroupIdentity.forProjectPrefix(projectId)))
+ loadByTriggerKeys(keys.toSet)
+ }
+
+ @Transactional(readOnly = true)
+ def jobsPerProjectStat: Map[Int, Long] = { // Map (ProjectId -> Requested Jobs count)
+ import GroupIdentity._
+ import Execution._
+ import scala.collection.JavaConversions._
+
+ val keys = scheduler.getTriggerKeys(GroupMatcher.groupStartsWith(GroupIdentity.basePrefix))
+ keys.
+ filter( isWorkflowExecution ).
+ groupBy ( projectId ).
+ map { case (projectId, triggerKeys) => (projectId, triggerKeys.size.toLong) }
+ }
private implicit def triggerMatcher(groupId: String): GroupMatcher[TriggerKey] = GroupMatcher.triggerGroupEquals(groupId)
private implicit def groupMatcher(groupId: String): GroupMatcher[JobKey] = GroupMatcher.jobGroupEquals(groupId)
View
2  frontend/src/main/resources/WEB-INF/spring/frontend-config.xml
@@ -61,7 +61,7 @@
<bean class="com.griddynamics.genesis.rest.EnvironmentsController"/>
- <bean class="com.griddynamics.genesis.rest.EnvironmentJobsController"/>
+ <bean class="com.griddynamics.genesis.rest.ScheduledJobsController"/>
<bean class="com.griddynamics.genesis.rest.GenesisRestController"/>
View
2  frontend/src/main/resources/WEB-INF/spring/security-config.xml
@@ -250,6 +250,8 @@
<s:intercept-url pattern="/metrics*" method="GET" access="hasRole('ROLE_GENESIS_ADMIN') or hasRole('ROLE_GENESIS_READONLY')"/>
<s:intercept-url pattern="/metrics/**" method="GET" access="hasRole('ROLE_GENESIS_ADMIN') or hasRole('ROLE_GENESIS_READONLY')"/>
+ <s:intercept-url pattern="/rest/jobs-stat" method="GET" access="hasRole('ROLE_GENESIS_ADMIN') or hasRole('ROLE_GENESIS_READONLY')"/>
+
<s:intercept-url pattern="/**" access="denyAll" />
<s:form-login login-page="/login.html"
View
6 frontend/src/main/scala/com/griddynamics/genesis/rest/RootController.scala
@@ -29,7 +29,11 @@ class RootController {
val links: Seq[Link] =
collectLinks(classOf[ProjectsController], classOf[Project], LinkTarget.COLLECTION) ++
- (if (isAdminOrReadOnly) collectLinks(classOf[SettingsController], classOf[SystemSettings], LinkTarget.COLLECTION) else Seq()) ++
+ (if (isAdminOrReadOnly)
+ collectLinks(classOf[SettingsController], classOf[SystemSettings], LinkTarget.COLLECTION) ++
+ Seq(LinkBuilder(HrefBuilder.absolutePath("rest/jobs-stat"), LinkTarget.COLLECTION, RequestMethod.GET))
+ else
+ Seq()) ++
(if (!isLogoutDisabled) Seq(LinkBuilder(HrefBuilder.absolutePath("logout"), LinkTarget.LOGOUT, RequestMethod.GET)) else Seq())
Map(
View
49 ...esis/rest/EnvironmentJobsController.scala → ...enesis/rest/ScheduledJobsController.scala
@@ -31,21 +31,27 @@ import javax.servlet.http.HttpServletRequest
import com.griddynamics.genesis.rest.GenesisRestController._
import java.util.Date
import com.griddynamics.genesis.rest.links.{ItemWrapper, WebPath, LinkBuilder, CollectionWrapper}
-import com.griddynamics.genesis.api.{ExtendedResult, Failure, Success, ScheduledJobDetails}
+import com.griddynamics.genesis.api.{ScheduledJobStat, ExtendedResult, Failure, Success, ScheduledJobDetails}
import java.util.concurrent.TimeUnit
import com.griddynamics.genesis.rest.annotations.{LinkTarget, AddSelfLinks}
import org.springframework.web.bind.annotation.RequestMethod._
import com.griddynamics.genesis.spring.security.LinkSecurityBean
+import com.griddynamics.genesis.rest.links.HrefBuilder._
+import com.griddynamics.genesis.api.ScheduledJobStat
+import com.griddynamics.genesis.rest.links.ItemWrapper
+import com.griddynamics.genesis.api.Failure
+import com.griddynamics.genesis.api.Success
+import com.griddynamics.genesis.api.ScheduledJobDetails
@Controller
-@RequestMapping(Array("/rest/projects/{projectId}/envs"))
-class EnvironmentJobsController {
+@RequestMapping(Array("/rest"))
+class ScheduledJobsController {
@Autowired var envConfigService: EnvironmentConfigurationService = _
@Autowired var schedulingService: EnvironmentJobService = _
@Autowired var executedJobs: ExecutedJobsService = _
@Autowired implicit var linkSecurity: LinkSecurityBean = _
- @RequestMapping(value = Array("{envId}/jobs"), method = Array(RequestMethod.POST))
+ @RequestMapping(value = Array("projects/{projectId}/envs/{envId}/jobs"), method = Array(RequestMethod.POST))
@ResponseBody
def scheduleWorkflow(@PathVariable("projectId") projectId: Int,
@PathVariable("envId") envId: Int,
@@ -65,7 +71,7 @@ class EnvironmentJobsController {
}
}
- @RequestMapping(value = Array("{envId}/jobs"), method = Array(RequestMethod.GET))
+ @RequestMapping(value = Array("projects/{projectId}/envs/{envId}/jobs"), method = Array(RequestMethod.GET))
@ResponseBody
@AddSelfLinks(methods = Array(GET, POST), modelClass = classOf[ScheduledJobDetails])
def scheduledJobs(@PathVariable("projectId") projectId: Int,
@@ -76,7 +82,7 @@ class EnvironmentJobsController {
job.withLinks(LinkBuilder(WebPath(request) / job.id, LinkTarget.SELF, classOf[ScheduledJobDetails], PUT, DELETE)).filtered())
}
- @RequestMapping(value = Array("{envId}/jobs/{jobId}"), method = Array(RequestMethod.DELETE))
+ @RequestMapping(value = Array("projects/{projectId}/envs/{envId}/jobs/{jobId}"), method = Array(RequestMethod.DELETE))
@ResponseBody
def removeJob(@PathVariable("projectId") projectId: Int,
@PathVariable("envId") envId: Int,
@@ -90,7 +96,7 @@ class EnvironmentJobsController {
}
}
- @RequestMapping(value = Array("{envId}/failedJobs"), method = Array(RequestMethod.GET))
+ @RequestMapping(value = Array("projects/{projectId}/envs/{envId}/failedJobs"), method = Array(RequestMethod.GET))
@ResponseBody
@AddSelfLinks(methods = Array(GET), modelClass = classOf[ScheduledJobDetails])
def failedJobs(@PathVariable("projectId") projectId: Int,
@@ -101,16 +107,41 @@ class EnvironmentJobsController {
job.withLinks(LinkBuilder(WebPath(request) / job.id, LinkTarget.SELF, classOf[ScheduledJobDetails], DELETE)).filtered())
}
- @RequestMapping(value = Array("{envId}/failedJobs/{jobId}"), method = Array(RequestMethod.DELETE))
+ @RequestMapping(value = Array("projects/{projectId}/envs/{envId}/failedJobs/{jobId}"), method = Array(RequestMethod.DELETE))
@ResponseBody
def removeFailedJob(@PathVariable("projectId") projectId: Int,
@PathVariable("envId") envId: Int,
@PathVariable("jobId") jobId: Int,
request: HttpServletRequest): ExtendedResult[_] = {
try {
- executedJobs.removeJobRecord(envId, jobId)
+ executedJobs.removeFailedJobRecord(envId, jobId)
} catch {
case e: Exception => Failure(compoundServiceErrors = Seq(e.getMessage))
}
}
+
+
+ @RequestMapping(value = Array("projects/{projectId}/jobs"), method = Array(RequestMethod.GET))
+ @ResponseBody
+ def projectJobs(@PathVariable("projectId") projectId: Int, request: HttpServletRequest) = {
+ val top = WebPath(absolutePath(s"/rest/projects/$projectId")(request))
+ import CollectionWrapper._
+ val (failed, requested) = executedJobs.listProjectJobs(projectId)
+ Map(
+ "failed" -> failed.map( a => a.withLinks(LinkBuilder(top / "envs" / a.envId / "failedJobs" / a.id, LinkTarget.SELF, classOf[ScheduledJobDetails], DELETE)).filtered()),
+ "requested" -> requested.map( a => a.withLinks(LinkBuilder(top / "envs" / a.envId / "jobs" / a.id, LinkTarget.SELF, classOf[ScheduledJobDetails], PUT, DELETE)).filtered())
+ )
+ }
+
+ @RequestMapping(value = Array("/jobs-stat"), method = Array(RequestMethod.GET))
+ @ResponseBody
+ def jobsStat(request: HttpServletRequest): CollectionWrapper[ItemWrapper[ScheduledJobStat]] = {
+ import CollectionWrapper._
+ executedJobs.jobsStat.map { s =>
+ val top = WebPath(absolutePath(s"/rest/projects/${s.projectId}")(request))
+ s.withLinks(
+ LinkBuilder(top / "jobs", LinkTarget.SELF, classOf[ScheduledJobDetails], GET)
+ ).filtered()
+ }
+ }
}
View
7 internal-api/src/main/scala/com/griddynamics/genesis/service/ExecutedJobsService.scala
@@ -22,9 +22,12 @@
*/
package com.griddynamics.genesis.service
-import com.griddynamics.genesis.api.{ExtendedResult, ScheduledJobDetails}
+import com.griddynamics.genesis.api.{ScheduledJobStat, ExtendedResult, ScheduledJobDetails}
trait ExecutedJobsService {
def listFailedJobs(envId: Int): Iterable[ScheduledJobDetails]
- def removeJobRecord(envId: Int, jobId: Int): ExtendedResult[Int]
+ def removeFailedJobRecord(envId: Int, jobId: Int): ExtendedResult[Int]
+ def removeFailedJobsRecords(projectId: Int, envId: Int): ExtendedResult[Int]
+ def jobsStat: Seq[ScheduledJobStat]
+ def listProjectJobs(projectId: Int): (Seq[ScheduledJobDetails], Seq[ScheduledJobDetails]) //(failed, scheduled)
}
View
22 test-automation/cucumber/templates/Simple.genesis
@@ -11,6 +11,28 @@ template {
}
}
+ workflow("update") {
+ steps {
+ execLocal {
+ phase = "init"
+ shell = "sh"
+ commands = ['echo "Creation of new environment..."']
+ }
+ execLocal {
+ phase = "postinit"
+ precedingPhases = ["init"]
+ shell = "sh"
+ commands = ['echo "Post init..." && /bin/nocommand']
+ }
+ execLocal {
+ phase = "finish"
+ precedingPhases = ["init", "postinit"]
+ shell = "sh"
+ commands = ['echo "Post init..." && /bin/nocommand']
+ }
+ }
+ }
+
workflow("destroy") {
steps {
}
View
6 ui/src/main/resources/genesis/app/app.js
@@ -11,17 +11,13 @@ require([
"modules/status",
"modules/projects",
"modules/environments",
- //"modules/env_details/env_details",
- //"modules/createenv",
"modules/breadcrumbs",
-// "modules/project_properties",
-// "cs!modules/settings/main",
//jquery plugins
"bootstrap",
"tabs"
],
-function(genesis, routermodule, jQuery, Backbone, _, backend, status, Projects, Environments, /*EnvironmentDetails,CreateEnvironment, */ Breadcrumbs /*,ProjectProperties, AppSettings*/) {
+function(genesis, routermodule, jQuery, Backbone, _, backend, status, Projects, Environments, Breadcrumbs ) {
var app = genesis.app;
View
7 ui/src/main/resources/genesis/app/modules/breadcrumbs.js
@@ -13,6 +13,7 @@ function(genesis, Backbone, Environments) {
"create_new_inst": "Create new instance",
"project_list": "Project list",
"system_settings": "System settings",
+ "dashboard": "Dashboard",
"project_properties": "Properties",
"create_project": "Create"
};
@@ -26,6 +27,7 @@ function(genesis, Backbone, Environments) {
var _homeLocation = _locationItem("/", LANG["project_list"]);
var _settingsLocation = _locationItem("settings", LANG["system_settings"]);
+ var _dashboardLocation = _locationItem("dashboard", LANG["dashboard"]);
var _createProjectLocation = _locationItem("admin/create/project", LANG["create_project"]);
@@ -54,6 +56,7 @@ function(genesis, Backbone, Environments) {
router.bind("route:createEnvironment", this.createEnvironment);
router.bind("route:listSettings", this.settings);
router.bind("route:createProject", this.createProject);
+ router.bind("route:dashboard", this.dashboard);
genesis.app.bind("breadcrumb:changed", this.updateLastLocation)
},
@@ -120,6 +123,10 @@ function(genesis, Backbone, Environments) {
this.render(locationList);
},
+ dashboard: function() {
+ this.render([_dashboardLocation]);
+ },
+
render: function(locationList) {
this.$el.html(this.template({breadcrumbs: locationList}));
}
View
111 ui/src/main/resources/genesis/app/modules/dashboard/jobs.coffee
@@ -0,0 +1,111 @@
+define [
+ "genesis",
+ "backbone",
+ "modules/environments"
+ "modules/validation",
+ "services/backend",
+ "utils/poller",
+ "jquery",
+ "momentjs",
+ "jvalidate"
+], (genesis, Backbone, Env, validation, backend, poller, $) ->
+ 'use strict'
+
+ Jobs = genesis.module()
+
+ class JobRecord extends Backbone.Model
+ idAttribute: "projectId"
+
+ class ProjectStats extends genesis.Backbone.Collection
+ url: "rest/jobs-stat"
+ model: JobRecord
+
+ class EnvsCollection extends Env.Collection
+ getFilter: ->
+ {
+ namePart: ""
+ statuses:
+ ready:
+ visible: true
+ busy:
+ visible: true
+ broken:
+ visible: true
+ }
+
+ class Job extends genesis.Backbone.Model
+ linkType: backend.LinkTypes.EnvScheduledJob
+ initialize: (options) ->
+ @url = options.urlLink
+
+ class JobView extends Backbone.View
+ template: "app/templates/dashboard/project_jobs.html"
+ initialize: (options) ->
+ jobs = options.jobs
+ @envs = options.envs
+ @projectId = options.projectId
+ @failedPerEnv = _.chain(jobs.get("failed")).map((i) -> body = i.item; body.links = i.links; body.status = "Failed"; body ).sortBy('date').groupBy("envId").value()
+ @requestedPerEnv = _.chain(jobs.get("requested")).map((i) -> body = i.item; body.links = i.links; body.status = "Requested"; body ).sortBy('date').groupBy("envId").value()
+ @jobs = _.chain().
+ union(_.keys(@failedPerEnv), _.keys(@requestedPerEnv)).
+ reduce(((memo, k) =>
+ memo[k] = _.union(@failedPerEnv[k] or [], @requestedPerEnv[k] or []) unless _.isUndefined(k)
+ memo), {}).
+ value()
+
+ render: ->
+ $.when(genesis.fetchTemplate(@template)).done (tmpl) =>
+ @$el.html(tmpl(
+ jobs: @jobs,
+ envs: @envs,
+ moment: moment,
+ projectId: @projectId
+ ))
+
+
+ class Jobs.Views.Main extends Backbone.View
+ template: "app/templates/dashboard/jobs.html"
+
+ events:
+ "click .project-stat": "showProjectDetails"
+
+ initialize: (options) ->
+ _.bind @render, this
+ @projectsCollection = options.projects
+ @projects = options.projects.reduce ((memo, proj) -> memo[proj.id] = proj.toJSON(); memo ), {}
+ @stat = new ProjectStats
+
+ showProjectDetails: (e) ->
+ projectId = $(e.currentTarget).attr("data-project-id")
+ project = @stat.get(projectId)
+
+ $(".toggle", e.currentTarget).toggleClass("expanded")
+ $projDetailsEl = @$(".history-details[ data-project-id=" +projectId + "]")
+ $projDetailsEl.toggleClass("visible")
+
+ link = _.find project.get("links"), (l) -> l.rel == "self"
+ jobs = new Job(urlLink: link.href)
+ envs = new EnvsCollection([], project: @projectsCollection.get(projectId))
+ $.when(jobs.fetch(), envs.fetch()).done =>
+ view = new JobView(
+ jobs: jobs,
+ envs: envs,
+ el: $projDetailsEl,
+ projectId: projectId
+ )
+ view.render()
+
+ onClose: ->
+
+ render: ->
+ $.when(genesis.fetchTemplate(@template), @stat.fetch()).done (tmpl) =>
+ numbers = @stat.reduce(((memo, j) => memo.failed += j.get('failedJobs'); memo.requested += j.get('scheduledJobs'); memo), {failed: 0, requested: 0})
+ @$el.html ( tmpl
+ stat: @stat.toJSON(),
+ scheduledJobsCount: numbers.requested,
+ failedJobsCount: numbers.failed,
+ projects: @projects
+ )
+
+ Jobs
+
View
4 ui/src/main/resources/genesis/app/modules/env_details/env_details.js
@@ -412,7 +412,6 @@ function (genesis, backend, poller, status, EnvHistory, variablesmodule, gtempla
$confirmDialog: view.$("#job-removal-confirm")
});
- view.scheduledJobsView.render();
view.scheduledJobsView.bind("request-job-update", function(job){
view.executeWorkflow(job.get('workflow'), job.get("date"), job.get('variables'));
});
@@ -424,8 +423,6 @@ function (genesis, backend, poller, status, EnvHistory, variablesmodule, gtempla
error: true
});
- view.failedJobsView.render();
-
}).fail(function() {
genesis.app.trigger("server-communication-error",
"Failed to get instance details<br/><br/> Please contact administrator.",
@@ -776,6 +773,7 @@ function (genesis, backend, poller, status, EnvHistory, variablesmodule, gtempla
accessRights: self.collection.itemAccessRights(),
error: self.error
}));
+ self.$el.show();
}
});
}
View
11 ui/src/main/resources/genesis/app/modules/env_details/history.js
@@ -225,11 +225,16 @@ function (genesis, Backbone, status, $) {
render: function(callback) {
var self = this;
$.when(genesis.fetchTemplate(this.template)).done(function (tmpl) {
+ var failedSteps = _.filter(self.model.get('steps'), function(step) {
+ return step.status == 'Failed';
+ });
+ _.each(failedSteps, function(step){
+ self.showStepActions(step.stepId);
+ });
var htmls = _.chain(self.actionViews).keys().reduce(function(memo, item) { //real hardcore!
memo[item] = self.actionViews[item].html();
return memo;
}, {}).value();
-
self.$el.html(tmpl({
workflow: self.model.toJSON(),
projectId: self.model.projectId || self.model.collection.projectId,
@@ -239,6 +244,10 @@ function (genesis, Backbone, status, $) {
utils: genesis.utils
}));
+ _.each(failedSteps, function(step){
+ self.showStepActions(step.stepId);
+ });
+
_.chain(self.actionViews).keys().each(function(stepId) {
self.actionViews[stepId].setElement(self.$("#step-"+ stepId + "-actions .subtable"));
self.actionViews[stepId].refresh();
View
9 ui/src/main/resources/genesis/app/router.js
@@ -11,12 +11,13 @@ define([
"modules/createenv",
"modules/project_properties",
"cs!modules/settings/main",
+ "cs!modules/dashboard/jobs",
//jquery plugins
"bootstrap",
"tabs"
],
-function(genesis, Backbone, _, Projects, Environments, EnvironmentDetails, CreateEnvironment, ProjectProperties, AppSettings) {
+function(genesis, Backbone, _, Projects, Environments, EnvironmentDetails, CreateEnvironment, ProjectProperties, AppSettings, Jobs) {
var rmodule = genesis.module();
@@ -33,6 +34,7 @@ function(genesis, Backbone, _, Projects, Environments, EnvironmentDetails, Creat
"project/:projectId/properties": "projectProperties",
"admin/create/project": "createProject",
"settings": "listSettings",
+ "dashboard": "dashboard",
":hash": "index",
"*invalid": "index"
},
@@ -105,6 +107,11 @@ function(genesis, Backbone, _, Projects, Environments, EnvironmentDetails, Creat
}
},
+ dashboard: function() {
+ this.setCurrentView(new Jobs.Views.Main({el: this.$viewDiv(), projects: this.projects}));
+
+ },
+
listSettings: function() {
this.setCurrentView(new AppSettings.Views.Main({el: this.$viewDiv()}));
},
View
29 ui/src/main/resources/genesis/app/templates/dashboard/jobs.html
@@ -0,0 +1,29 @@
+<div id="top-panel" style="background-color: white" >
+ <div id="page-title" style="padding-top: 25px;">
+ <img id="env-icon" src="assets/img/calendar2.png" alt="Env">
+ <h1 class="envname">Scheduled jobs</h1>
+ </div>
+ <div style="float: right; padding-top: 25px; ">
+ <h2 style="font-size: 14px; ">Total Scheduled: <strong><%- scheduledJobsCount %></strong> &nbsp;&nbsp; / &nbsp;&nbsp; Total Failed: <strong <% if ( failedJobsCount > 0 ) { %> style="color:red" <% } %> ><%- failedJobsCount %></strong> </h2>
+ </div>
+</div>
+
+<div id ="tab-panel" style="margin-top: 0">
+ <div id="panel" style="border-top: none">
+
+ <% for (var i = 0; i < stat.length; i ++) { var record = stat[i]; var project = projects[record.projectId] %>
+ <div class="toggle-sector project-stat" data-project-id="<%- record.projectId %>" style="margin-top: 10px">
+ <h3 class="job-details toggle" > Project '<%- project? project.name : '' %>'</h3>
+ <div class="right-column">
+ <span> Scheduled : <strong><%- record.scheduledJobs %></strong></span> &nbsp;&nbsp; / &nbsp;&nbsp;
+ <span> Failed : <strong <% if ( record.failedJobs > 0 ) { %> style="color:red" <% } %> ><%- record.failedJobs %></strong></span>
+ </div>
+ <div class="clear"></div>
+ </div>
+
+ <div class="history-details tasks" data-project-id="<%- record.projectId %>">
+ </div>
+ <% } %>
+
+ </div>
+</div>
View
17 ui/src/main/resources/genesis/app/templates/dashboard/project_jobs.html
@@ -0,0 +1,17 @@
+<% _.forEach(_.keys(jobs), function(envId){ %>
+ <div class="instance">
+ <span>Instance <a href="project/<%= projectId %>/inst/<%= envId %>" style="text-decoration: underline; font-weight: bold">'<%- envs.get(envId).get('name') %>'</a></span>
+ </div>
+ <% for(var i = 0, count = jobs[envId].length; i < count; i++) { var job = jobs[envId][i] %>
+ <div class="task <%= job.status === 'Failed' ? 'failed': 'requested' %>">
+ <div class="desc">
+ <div class="title">Workflow <strong class="capitalize" >'<%- job.workflow %>'</strong> &nbsp;&nbsp;&nbsp;&nbsp; <em> by <%= job.scheduledBy %> </em></div>
+ </div>
+ <div class="time">
+ <div class="date"> <%= moment(job.date).format('lll') %> </div>
+ <div> <%= moment(job.date).fromNow() %> </div>
+ </div>
+ <div style="clear:both"/>
+ </div>
+ <% } %>
+<% }) %>
View
4 ui/src/main/resources/genesis/app/templates/env_details.html
@@ -79,10 +79,10 @@ <h1 class="envname"><%= environment.name %></h1>
<div id="panel-tab-1" class="tab-content opened">
<div id="env-attrs"></div>
- <div id="scheduled-executions">
+ <div id="scheduled-executions" style="display: none">
</div>
- <div id="failed-executions">
+ <div id="failed-executions" style="display: none">
</div>
</div>
View
53 ui/src/main/resources/genesis/assets/css/style.css
@@ -158,6 +158,10 @@ a.button.reset-button {
display: block;
}
+.capitalize {
+ text-transform: capitalize;
+}
+
a.tab {
margin-right: 5px;
display: block;
@@ -841,7 +845,7 @@ img#addShare{
vertical-align:middle;
}
-#wrapper #wrapper-content #tab-panel #panel #panel-tab-3 .workflow{
+#wrapper #wrapper-content #tab-panel #panel #panel-tab-3 .workflow, .toggle-sector {
width: 100%;
min-height: 23px;
padding-top: 2px;
@@ -870,6 +874,51 @@ img.toggle {
margin: 2px 5px 2px 5px;
}
+.toggle-sector {
+ min-height: 25px !important;
+}
+.toggle-sector h3 {
+ font-size: 13px;
+}
+.tasks .instance {
+ background: rgb(245, 245, 245);
+ margin-bottom: 1px;
+}
+.tasks .instance span {
+ background: #E8E8E8;
+ padding: 2px 10px;
+ display: inline-block;
+ font-size: 12px;
+ border-top: 1px solid #f9f9f9;
+}
+.task.requested {
+ border-left: 2px solid #E8E8E8;
+}
+.task.failed {
+ border-left: 2px solid #B03040;
+ color: #B03040;
+}
+.task {
+ border-bottom: 1px solid #f9f9f9;
+ margin-bottom: 1px;
+}
+
+.task .desc {
+ display: inline-block;
+ width: 75%;
+ padding: 10px 10px;
+ font-size: 12px;
+}
+.task .time {
+ display: inline-block;
+ float: right;
+ width: 15%;
+ padding: 2px 10px 5px 0;
+ font-size: 12px;
+ text-align: right;
+}
+
+
.workflow h3.toggle, .step-details-expander a.toggle, .config_group h3.toggle, .job-details.toggle {
margin-top: 3px;
padding-left: 12px;
@@ -1083,7 +1132,7 @@ td.subtable{
display: inline-block;
min-width: 100px;
}
-#wrapper #wrapper-content #tab-panel #panel #panel-tab-3 .workflow .workflow-actions{
+#wrapper #wrapper-content #tab-panel #panel #panel-tab-3 .workflow .workflow-actions, .toggle-sector .right-column {
float:right;
margin-top:3px;
margin-right:10px;
View
BIN  ui/src/main/resources/genesis/assets/img/calendar2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
148 ui/src/main/resources/genesis/index.html
@@ -1,21 +1,21 @@
<!DOCTYPE html>
<html>
<head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <link rel="shortcut icon" type="image/x-icon" href="favicon.ico"/>
- <link rel="stylesheet" type="text/css" href="assets/css/index.css"/>
- <script data-main="app/requirejs-config" src="assets/js/libs/require.js"></script>
- <title>Genesis</title>
- <script type="text/javascript">
- (function checkIEVersion() {
- var userAgent = navigator.userAgent;
- var match = userAgent.match('MSIE (.)');
- if(match && match.length > 1) {
- var version = parseInt(match[1]);
- var docMode = document.documentMode;
- if (version < 9 || docMode && (docMode < 9)) window.location = 'ie_version_error.html';
- }
- }) ();
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <link rel="shortcut icon" type="image/x-icon" href="favicon.ico"/>
+ <link rel="stylesheet" type="text/css" href="assets/css/index.css"/>
+ <script data-main="app/requirejs-config" src="assets/js/libs/require.js"></script>
+ <title>Genesis</title>
+ <script type="text/javascript">
+ (function checkIEVersion() {
+ var userAgent = navigator.userAgent;
+ var match = userAgent.match('MSIE (.)');
+ if(match && match.length > 1) {
+ var version = parseInt(match[1]);
+ var docMode = document.documentMode;
+ if (version < 9 || docMode && (docMode < 9)) window.location = 'ie_version_error.html';
+ }
+ }) ();
</script>
</head>
@@ -23,66 +23,66 @@
<script id="breadcrumbs_list_template" type="text/x-underscore-tmplate">
<% for(var i=0, count = breadcrumbs.length; i < count; i++) { var item = breadcrumbs[i] %>
- <% if ( i + 1 < count || count == 1) { %>
- <a href="<%= (item.url.indexOf('#') == 0? item.url : '#' + item.url) %>" class="brcr"> <%= item.title %> </a>
- <% if (count != 1) { %>
- <span class='brcr-devider'><img src="assets/img/brcr-divider.png"></span>
- <% } %>
- <% } else { %>
- <div class="brcr-last-item"><span > <%= item.title %> </span></div>
- <% } %>
+ <% if ( i + 1 < count || count == 1) { %>
+ <a href="<%= (item.url.indexOf('#') == 0? item.url : '#' + item.url) %>" class="brcr"> <%= item.title %> </a>
+ <% if (count != 1) { %>
+ <span class='brcr-devider'><img src="assets/img/brcr-divider.png"></span>
+ <% } %>
+ <% } else { %>
+ <div class="brcr-last-item"><span > <%= item.title %> </span></div>
+ <% } %>
<% } %>
</script>
<script id="project-dropdown-list-tmpl" type="text/x-underscore-tmplate">
<% if (projects.length > 0) { %>
- <% for (var i = 0, count = projects.length; i < count; i++) { var project = projects[i]; %>
- <!--<a href="project/<%= project.id %>"><%= project.name %></a><br>-->
- <li><a href="project/<%= project.id %>"><%= project.name %></a></li>
- <% } %>
+ <% for (var i = 0, count = projects.length; i < count; i++) { var project = projects[i]; %>
+ <!--<a href="project/<%= project.id %>"><%= project.name %></a><br>-->
+ <li><a href="project/<%= project.id %>"><%= project.name %></a></li>
+ <% } %>
<% } else { %>
- <li ><p style="color: #000000; margin: 5px;">No projects available</p></li>
+ <li ><p style="color: #000000; margin: 5px;">No projects available</p></li>
<% } %>
<li class="divider"></li>
<li><a href="/">View All Projects</a></li>
</script>
<script id="app-growl-message" type="text/x-underscore-tmplate">
- <div class="<%= message.type %> ui-notify-message ui-notify-message-style">
- <a data-bypass class="ui-notify-close" href="javascript:void(0)" title="Close notification">
- <img src="assets/img/cross_grey_small.png" alt="close">
- </a>
- <% if (message.title) { %>
- <h1><%= message.title %></h1>
- <% } %>
- <% var messageClass = _.isUndefined(message.title) ? "no-title" : ""; %>
- <% if (!_.isArray(message.text)) { %>
+ <div class="<%= message.type %> ui-notify-message ui-notify-message-style">
+ <a data-bypass class="ui-notify-close" href="javascript:void(0)" title="Close notification">
+ <img src="assets/img/cross_grey_small.png" alt="close">
+ </a>
+ <% if (message.title) { %>
+ <h1><%= message.title %></h1>
+ <% } %>
+ <% var messageClass = _.isUndefined(message.title) ? "no-title" : ""; %>
+ <% if (!_.isArray(message.text)) { %>
<p class='<%= messageClass %>' >
- <%= message.text %>
+ <%= message.text %>
</p>
- <% } else { %>
+ <% } else { %>
<ul class='<%= messageClass %>'>
- <% for (var i = 0, count = message.text.length; i < count; i++) { var msg = message.text[i]; %>
+ <% for (var i = 0, count = message.text.length; i < count; i++) { var msg = message.text[i]; %>
<li><%- msg %></li>
- <% } %>
+ <% } %>
</ul>
- <% } %>
- </div>
+ <% } %>
+ </div>
</script>
<div id="page-view-loading" style="display: block; ">
- <img src="assets/img/ajax-loader.gif" />
- <div class="orig"></div>
+ <img src="assets/img/ajax-loader.gif" />
+ <div class="orig"></div>
</div>
<div id="overlay"></div>
<div id="growl-container" style="display:none" class="ui-notify"></div>
<div id="page">
- <div id="page-content">
- <div id="header">
- <div id="connection-error"> No connection to server </div>
- <div id="header-content">
+ <div id="page-content">
+ <div id="header">
+ <div id="connection-error"> No connection to server </div>
+ <div id="header-content">
<a id="fake-logo-link" href="#" ></a>
<div id="userinfo">
<div id="project-list" rel="p2" >
@@ -95,11 +95,23 @@
</ul>
</div>
</div>
- <div class="system-settings" style="display:none">
- <a href="#settings" style="display: inline-block; color: white;" >System Settings...</a>
- </div>
+ <div class="system-settings" style="display:none">
+
+ <div class="dropdown">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#menu1">
+ <span id="current-project" class="btn">System</span>
+ <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu ">
+ <li ><a href="#settings" style="" >Settings...</a></li>
+ <li ><a href="#dashboard" style="" >Dashboard...</a></li>
+ </ul>
+ </div>
+
+ <!--<a href="#settings" style="display: inline-block; color: white;" >System Settings...</a>-->
+ </div>
- <div class="user-section" rel="p1">
+ <div class="user-section" rel="p1">
<div id="logout_elt" class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#menu1">
User: &nbsp;&nbsp;<span id="btn1" class="user-name" >...</span>
@@ -113,28 +125,28 @@
</div>
</div>
</div>
-
+
</div>
</div>
<div id="wrapper">
- <div id="breadcrumbs"></div>
- <div class="bubble" >
- <div id="wrapper-content" ></div>
- <div class="empty"></div>
- </div>
+ <div id="breadcrumbs"></div>
+ <div class="bubble" >
+ <div id="wrapper-content" ></div>
+ <div class="empty"></div>
+ </div>
</div>
- </div>
+ </div>
</div>
<div id="footer">
- <div id="copyright">
- &copy; 2013 Grid Dynamics. All rights reserved
- </div>
- <div id="footer-links">
- <a target="_blank" href="http://github.com/griddynamics/OpenGenesis">About Genesis</a>
- <span class="genesis-version">(Core: <span class="core-details">N/A</span>; Distribution: <span class="distr-details">N/A</span>)</span>
- <!--<a href="#">Feedback</a>-->
- </div>
+ <div id="copyright">
+ &copy; 2013 Grid Dynamics. All rights reserved
+ </div>
+ <div id="footer-links">
+ <a target="_blank" href="http://github.com/griddynamics/OpenGenesis">About Genesis</a>
+ <span class="genesis-version">(Core: <span class="core-details">N/A</span>; Distribution: <span class="distr-details">N/A</span>)</span>
+ <!--<a href="#">Feedback</a>-->
+ </div>
</div>
</body>

No commit comments for this range

Something went wrong with that request. Please try again.