azkaban-developers
Changes
.gitignore 6(+6 -0)
build.gradle 392(+392 -0)
build.xml 6(+3 -3)
ivy/.gitignore 2(+1 -1)
ivy/ivysettings.xml 2(+1 -1)
ivy/libraries.properties 2(+1 -1)
src/main/java/azkaban/flow/Edge.java 0(+0 -0)
src/main/java/azkaban/flow/Flow.java 0(+0 -0)
src/main/java/azkaban/flow/Node.java 0(+0 -0)
src/main/java/azkaban/user/Role.java 0(+0 -0)
src/main/java/azkaban/user/User.java 0(+0 -0)
src/main/less/.gitignore 1(+1 -0)
src/main/less/azkaban.less 0(+0 -0)
src/main/less/azkaban-graph.less 165(+165 -0)
src/main/less/base.less 160(+160 -0)
src/main/less/callout.less 0(+0 -0)
src/main/less/context-menu.less 0(+0 -0)
src/main/less/flow.less 342(+342 -0)
src/main/less/header.less 0(+0 -0)
src/main/less/log.less 0(+0 -0)
src/main/less/login.less 0(+0 -0)
src/main/less/Makefile 0(+0 -0)
src/main/less/navbar.less 121(+121 -0)
src/main/less/non-responsive.less 88(+88 -0)
src/main/less/off-canvas.less 0(+0 -0)
src/main/less/project.less 142(+142 -0)
src/main/less/tables.less 149(+149 -0)
src/main/less/variables.less 0(+0 -0)
src/main/resources/log4j.properties 0(+0 -0)
src/main/tl/.gitignore 1(+1 -0)
src/main/tl/flowstats.tl 155(+155 -0)
src/main/tl/flowstats-no-data.tl 0(+0 -0)
src/main/tl/flowsummary.tl 69(+69 -0)
src/main/tl/Makefile 0(+0 -0)
Details
.gitignore 6(+6 -0)
diff --git a/.gitignore b/.gitignore
index 1908b60..553b1a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,14 @@
dist/
build/
+.gradle/
+.settings/
+node_modules/
.idea/
*.iml
*.log
TestProcess_*
_AzkabanTestDir_*
reports/
+/bin
+.classpath
+.project
build.gradle 392(+392 -0)
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..a013b46
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,392 @@
+apply plugin: 'java'
+apply plugin: 'eclipse'
+
+/**
+ * Helper that calls a command and returns the output
+ */
+def cmdCaller = { commandln ->
+ def stdout = new ByteArrayOutputStream()
+ exec {
+ commandLine commandln
+ standardOutput = stdout
+ }
+
+ return stdout.toString().trim()
+}
+
+/**
+ * Git version name from git tag
+ */
+def getVersionName = { ->
+ return cmdCaller(['git', 'describe', '--tags', '--abbrev=0'])
+}
+
+
+version = getVersionName()
+archivesBaseName = 'azkaban'
+check.dependsOn.remove(test)
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+}
+
+configurations {
+ all {
+ // We don't want the kitchen sink for dependencies. Only the ones we know we need for
+ // compile and ones we need to package.
+ transitive = false
+ }
+ compile {
+ description = 'compile classpath'
+ }
+ test {
+ extendsFrom compile
+ }
+}
+configurations.compile {
+ description = 'compile classpath'
+}
+
+dependencies {
+ compile (
+ [group: 'commons-collections', name:'commons-collections', version: '3.2.1'],
+ [group: 'commons-configuration', name:'commons-configuration', version: '1.8'],
+ [group: 'commons-dbcp', name:'commons-dbcp', version: '1.4'],
+ [group: 'commons-dbutils', name:'commons-dbutils', version: '1.5'],
+ [group: 'org.apache.commons', name:'commons-email', version: '1.2'],
+ [group: 'commons-fileupload', name:'commons-fileupload', version: '1.2.1'],
+ [group: 'commons-io', name:'commons-io', version: '2.4'],
+ [group: 'org.apache.commons', name:'commons-jexl', version: '2.1.1'],
+ [group: 'commons-lang', name:'commons-lang', version: '2.6'],
+ [group: 'commons-logging', name:'commons-logging', version: '1.1.1'],
+ [group: 'commons-pool', name:'commons-pool', version: '1.6'],
+ [group: 'com.google.guava', name:'guava', version: '13.0.1'],
+ [group: 'com.h2database', name:'h2', version: '1.3.170'],
+ [group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.2.1'],
+ [group: 'org.apache.httpcomponents', name:'httpcore', version: '4.2.1'],
+ [group: 'org.codehaus.jackson', name:'jackson-core-asl', version: '1.9.5'],
+ [group: 'org.codehaus.jackson', name:'jackson-mapper-asl',version: '1.9.5'],
+ [group: 'org.codehaus.jackson', name:'jackson-core-asl', version: '1.9.5'],
+ [group: 'org.mortbay.jetty', name:'jetty', version: '6.1.26'],
+ [group: 'org.mortbay.jetty', name:'jetty-util', version: '6.1.26'],
+ [group: 'joda-time', name:'joda-time', version: '2.0'],
+ [group: 'net.sf.jopt-simple', name:'jopt-simple', version: '4.3'],
+ [group: 'log4j', name:'log4j', version: '1.2.16'],
+ [group: 'javax.mail', name:'mail', version: '1.4.5'],
+ [group: 'mysql', name:'mysql-connector-java', version: '5.1.28'],
+ [group: 'javax.servlet', name:'servlet-api', version: '2.5'],
+ [group: 'org.slf4j', name:'slf4j-api', version: '1.6.1'],
+ [group: 'org.apache.velocity', name:'velocity', version: '1.7']
+ )
+
+ testCompile (
+ [group: 'junit', name:'junit', version: '4.11']
+ )
+}
+
+jar {
+ baseName = 'azkaban'
+ manifest {
+ attributes(
+ 'Implementation-Title': 'Azkaban',
+ 'Implementation-Version': version
+ )
+ }
+}
+
+eclipse.classpath.file {
+ // Erase the whole classpath
+ beforeMerged {
+ classpath -> classpath.entries.removeAll { entry -> true }
+ }
+
+ // We want to make sure that if there is an entry for src, that it doesn't have any
+ // include parameters
+ whenMerged { classpath ->
+ classpath.entries.findAll { entry -> entry.kind == 'src' }*.includes = []
+ }
+}
+
+/**
+ * Invokes a makefile target that will compile less files
+ */
+task compileLess(type:Exec) {
+ workingDir 'src/main/less'
+ commandLine 'make', '-e'
+ environment (
+ OBJ_DIR : file(new File(buildDir,'/less'))
+ )
+}
+
+/**
+ * Invokes a makefile target that will compile dust files
+ */
+task compileDust(type:Exec) {
+ workingDir 'src/main/tl'
+ commandLine 'make', '-e'
+ environment (
+ OBJ_DIR : file(new File(buildDir,'/dust'))
+ )
+}
+
+/**
+ * Copies web files to a build directory
+ */
+task web(dependsOn: ['compileLess', 'compileDust']) << {
+ println 'Copying web files'
+ copy {
+ from('src/web')
+ into('build/web')
+ }
+
+ copy {
+ from('build/dust')
+ into('build/web/js')
+ }
+ copy {
+ from('build/less')
+ into('build/web/css')
+ }
+}
+
+/*
+ * Gets the version name from the latest Git tag
+ */
+task createVersionFile() << {
+ String gitCommitHash = cmdCaller(['git', 'rev-parse', 'HEAD']);
+ String gitRepo = cmdCaller(['git', 'config', '--get', 'remote.origin.url']);
+ def date = new Date()
+ def formattedDate = date.format('yyyy-MM-dd hh:mm zzz')
+
+ String versionStr = version + '\n' +
+ gitCommitHash + '\n' +
+ gitRepo + '\n' +
+ formattedDate + '\n'
+
+ File versionFile = file('build/package/version.file')
+ versionFile.parentFile.mkdirs()
+ versionFile.write(versionStr)
+}
+
+/**
+ * Packages the SoloServer version of Azkaban
+ */
+task packageSolo(type: Tar, dependsOn: [jar, 'web', 'createVersionFile']) {
+ appendix = 'solo-server'
+ packageDir = 'build/package/' + baseName + '-' + appendix
+
+ println 'Creating Azkaban Solo Server Package into ' + packageDir
+ mkdir packageDir
+ mkdir packageDir + '/extlib'
+ mkdir packageDir + '/plugins'
+
+ println 'Copying Soloserver bin & conf'
+ copy {
+ from('src/package/soloserver')
+ into(packageDir)
+ }
+
+ println 'Copying Azkaban lib'
+ copy {
+ from('build/libs')
+ into(packageDir + '/lib')
+ }
+
+ println 'Copying web'
+ copy {
+ from('build/web')
+ into(packageDir + '/web')
+ }
+
+ println 'Copying sql'
+ copy {
+ from('src/sql')
+ into(packageDir + '/sql')
+ }
+
+ println 'Copying dependency jars'
+ copy {
+ into packageDir + '/lib'
+ from configurations.compile
+ }
+
+ copy {
+ into packageDir
+ from 'build/package/version.file'
+ }
+
+ println 'Tarballing Solo Package'
+ extension = 'tar.gz'
+ compression = Compression.GZIP
+
+ basedir = baseName + '-' + appendix + '-' + version
+ println 'Source is in ' + packageDir
+ into(basedir) {
+ from packageDir
+ exclude 'bin'
+ }
+
+ dst_bin = basedir + '/bin'
+ src_bin = packageDir + '/bin'
+ from(src_bin) {
+ into dst_bin
+ fileMode = 0755
+ }
+}
+
+/**
+ * Packages the Sql Scripts for Azkaban DB
+ */
+task packageSql(type: Tar) {
+ String packageDir = 'build/package/sql'
+
+ println 'Creating Azkaban SQL Scripts into ' + packageDir
+ mkdir packageDir
+
+ println 'Copying SQL files'
+ copy {
+ from('src/sql')
+ into(packageDir)
+ }
+
+ String destFile = packageDir + '/create-all-sql-' + version + '.sql';
+ println('Concating create scripts to ' + destFile)
+ ant.concat(destfile:destFile, fixlastline:'yes') {
+ fileset(dir: 'src/sql') {
+ exclude(name: 'update.*.sql')
+ exclude(name: 'database.properties')
+ }
+ }
+
+ println 'Tarballing SQL Package'
+ extension = 'tar.gz'
+ compression = Compression.GZIP
+ appendix = 'sql'
+
+ basedir = baseName + '-' + appendix + '-' + version
+ packageDir = 'build/package/sql'
+ println 'Source is in ' + packageDir
+ into(basedir) {
+ from packageDir
+ }
+}
+
+/**
+ * Packages the Azkaban Executor Server
+ */
+task packageExec(type: Tar, dependsOn: [jar, 'createVersionFile']) {
+ appendix = 'exec-server'
+ String packageDir = 'build/package/' + baseName + '-' + appendix
+
+ println 'Creating Azkaban Executor Server Package into ' + packageDir
+ mkdir packageDir
+ mkdir packageDir + '/extlib'
+ mkdir packageDir + '/plugins'
+
+ println 'Copying Exec server bin & conf'
+ copy {
+ from('src/package/execserver')
+ into(packageDir)
+ }
+
+ println 'Copying Azkaban lib'
+ copy {
+ from('build/libs')
+ into(packageDir + '/lib')
+ }
+
+ println 'Copying dependency jars'
+ copy {
+ into packageDir + '/lib'
+ from configurations.compile
+ }
+
+ copy {
+ into packageDir
+ from 'build/package/version.file'
+ }
+
+ println 'Tarballing Web Package'
+ extension = 'tar.gz'
+ compression = Compression.GZIP
+
+ basedir = baseName + '-' + appendix + '-' + version
+ packageDir = 'build/package/' + baseName + '-' + appendix
+ println 'Source is in ' + packageDir
+
+ into(basedir) {
+ from packageDir
+ exclude 'bin'
+ }
+
+ dst_bin = basedir + '/bin'
+ src_bin = packageDir + '/bin'
+ from(src_bin) {
+ into dst_bin
+ fileMode = 0755
+ }
+}
+
+/**
+ * Packages the Azkaban Web Server
+ */
+task packageWeb(type: Tar, dependsOn: [jar, 'web', 'createVersionFile']) {
+ appendix = 'web-server'
+ String packageDir = 'build/package/' + baseName + '-' + appendix
+
+ println 'Creating Azkaban Web Server Package into ' + packageDir
+ mkdir packageDir
+ mkdir packageDir + '/extlib'
+ mkdir packageDir + '/plugins'
+
+ println 'Copying Web server bin & conf'
+ copy {
+ from('src/package/webserver')
+ into(packageDir)
+ }
+
+ println 'Copying Azkaban lib'
+ copy {
+ from('build/libs')
+ into(packageDir + '/lib')
+ }
+
+ println 'Copying web'
+ copy {
+ from('build/web')
+ into(packageDir + '/web')
+ }
+
+ println 'Copying dependency jars'
+ copy {
+ into packageDir + '/lib'
+ from configurations.compile
+ }
+
+ copy {
+ into packageDir
+ from 'build/package/version.file'
+ }
+
+ println 'Tarballing Web Package'
+ extension = 'tar.gz'
+ compression = Compression.GZIP
+
+ basedir = baseName + '-' + appendix + '-' + version
+ println 'Source is in ' + packageDir
+ into(basedir) {
+ from packageDir
+ exclude 'bin'
+ }
+
+ dst_bin = basedir + '/bin'
+ src_bin = packageDir + '/bin'
+ from(src_bin) {
+ into dst_bin
+ fileMode = 0755
+ }
+}
+
+task packageAll(dependsOn : ['packageWeb', 'packageExec', 'packageSolo', 'packageSql']) {
+}
\ No newline at end of file
build.xml 6(+3 -3)
diff --git a/build.xml b/build.xml
index 1435ccf..bdbc243 100644
--- a/build.xml
+++ b/build.xml
@@ -21,9 +21,9 @@
<property name="solo.package.dir" value="${basedir}/src/package/soloserver" />
<property name="bin.dir" value="${basedir}/bin" />
- <property name="java.src.dir" value="${basedir}/src/java" />
- <property name="dust.src.dir" value="${basedir}/src/tl" />
- <property name="less.src.dir" value="${basedir}/src/less" />
+ <property name="java.src.dir" value="${basedir}/src/main/java" />
+ <property name="dust.src.dir" value="${basedir}/src/main/tl" />
+ <property name="less.src.dir" value="${basedir}/src/main/less" />
<property name="web.src.dir" value="${basedir}/src/web" />
<property name="sql.src.dir" value="${basedir}/src/sql" />
ivy/.gitignore 2(+1 -1)
diff --git a/ivy/.gitignore b/ivy/.gitignore
index d392f0e..f23b948 100644
--- a/ivy/.gitignore
+++ b/ivy/.gitignore
@@ -1 +1 @@
-*.jar
+*.jar
\ No newline at end of file
ivy/ivysettings.xml 2(+1 -1)
diff --git a/ivy/ivysettings.xml b/ivy/ivysettings.xml
index 26bb444..ea6d163 100644
--- a/ivy/ivysettings.xml
+++ b/ivy/ivysettings.xml
@@ -24,4 +24,4 @@
<resolver ref="oss-sonatype" />
</chain>
</resolvers>
-</ivysettings>
+</ivysettings>
\ No newline at end of file
ivy/libraries.properties 2(+1 -1)
diff --git a/ivy/libraries.properties b/ivy/libraries.properties
index ad9f00d..a8cd72f 100644
--- a/ivy/libraries.properties
+++ b/ivy/libraries.properties
@@ -29,4 +29,4 @@ servlet-api.version=2.5
slf4j-api.version=1.6.1
slf4j-log4j12.version=1.6.4
velocity.version=1.7
-velocity-tools.version=2.0
+velocity-tools.version=2.0
\ No newline at end of file
diff --git a/src/main/java/azkaban/webapp/session/SessionCache.java b/src/main/java/azkaban/webapp/session/SessionCache.java
new file mode 100644
index 0000000..7591dbf
--- /dev/null
+++ b/src/main/java/azkaban/webapp/session/SessionCache.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012 LinkedIn Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.webapp.session;
+
+import azkaban.utils.Props;
+import azkaban.utils.cache.Cache;
+import azkaban.utils.cache.CacheManager;
+import azkaban.utils.cache.Cache.EjectionPolicy;
+
+
+/**
+ * Cache for web session.
+ *
+ * The following global azkaban properties can be used: max.num.sessions - used
+ * to determine the number of live sessions that azkaban will handle. Default is
+ * 10000 session.time.to.live -Number of seconds before session expires.
+ * Default set to 1 days.
+ */
+public class SessionCache {
+ private static final int MAX_NUM_SESSIONS = 10000;
+ private static final long SESSION_TIME_TO_LIVE = 24*60*60*1000L;
+
+// private CacheManager manager = CacheManager.create();
+ private Cache cache;
+
+ /**
+ * Constructor taking global props.
+ *
+ * @param props
+ */
+ public SessionCache(Props props) {
+ CacheManager manager = CacheManager.getInstance();
+
+ cache = manager.createCache();
+ cache.setEjectionPolicy(EjectionPolicy.LRU);
+ cache.setMaxCacheSize(props.getInt("max.num.sessions", MAX_NUM_SESSIONS));
+ cache.setExpiryTimeToLiveMs(props.getLong("session.time.to.live", SESSION_TIME_TO_LIVE));
+ }
+
+ /**
+ * Returns the cached session using the session id.
+ *
+ * @param sessionId
+ * @return
+ */
+ public Session getSession(String sessionId) {
+ Session elem = cache.<Session>get(sessionId);
+
+ return elem;
+ }
+
+ /**
+ * Adds a session to the cache. Accessible through the session ID.
+ *
+ * @param id
+ * @param session
+ */
+ public void addSession(Session session) {
+ cache.put(session.getSessionId(), session);
+ }
+
+ /**
+ * Removes the session from the cache.
+ *
+ * @param id
+ * @return
+ */
+ public boolean removeSession(String id) {
+ return cache.remove(id);
+ }
+}
\ No newline at end of file
src/main/less/.gitignore 1(+1 -0)
diff --git a/src/main/less/.gitignore b/src/main/less/.gitignore
new file mode 100644
index 0000000..2416a67
--- /dev/null
+++ b/src/main/less/.gitignore
@@ -0,0 +1 @@
+obj/
src/main/less/azkaban-graph.less 165(+165 -0)
diff --git a/src/main/less/azkaban-graph.less b/src/main/less/azkaban-graph.less
new file mode 100644
index 0000000..50d818c
--- /dev/null
+++ b/src/main/less/azkaban-graph.less
@@ -0,0 +1,165 @@
+.nodebox {
+ text {
+ pointer-events: none;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ image {
+ pointer-events: none;
+ }
+
+ > .border:hover {
+ fill-opacity: 0.7;
+ }
+
+ > .flowborder:hover {
+ stroke-opacity: 0.7;
+ }
+}
+
+/* Nodes */
+.node {
+ &:hover {
+ cursor: pointer;
+ }
+
+ &.selected > .nodebox .border {
+ stroke-width: 3;
+ stroke: #39b3d7;
+ }
+
+ &.selected > .nodebox .flowborder {
+ stroke-width: 3;
+ fill: #D9EDFF;
+ }
+}
+
+.border {
+ stroke-width: 1;
+}
+
+.flownode .nodebox .flowborder {
+ stroke-width: 1.25;
+ fill: #FFF;
+ fill-opacity: 0.8;
+}
+
+.READY > g > rect {
+ fill: #DDD;
+ stroke: #CCC;
+}
+
+.READY > g > text {
+ fill: #000;
+}
+
+.RUNNING > g > rect {
+ fill: #39b3d7;
+ stroke: #39b3d7;
+}
+
+.RUNNING > g > text {
+ fill: #FFF;
+}
+
+.SUCCEEDED > g > rect {
+ fill: #5cb85c;
+ stroke: #4cae4c;
+}
+
+.SUCCEEDED > g > text {
+ fill: #FFF;
+}
+
+.FAILED > g > rect {
+ fill: #d2322d;
+ stroke: #d2322d;
+}
+
+.FAILED > g > text {
+ fill: #FFF;
+}
+
+.KILLED > g > rect {
+ fill: #d2322d;
+ stroke: #d2322d;
+}
+
+.KILLED > g > text {
+ fill: #FFF;
+}
+
+.CANCELLED > g > rect {
+ fill: #FF9999;
+ stroke: #FF9999;
+}
+
+.CANCELLED > g > text {
+ fill: #FFF;
+}
+
+.FAILED_FINISHING > g > rect {
+ fill: #ed9c28;
+ stroke: #ed9c28;
+}
+
+.FAILED_FINISHING > g > text {
+ fill: #FFF;
+}
+
+.DISABLED > g > rect {
+ fill: #DDD;
+ stroke: #CCC;
+}
+
+.DISABLED > g > rect {
+ fill: #DDD;
+ stroke: #CCC;
+}
+
+.nodeDisabled {
+ opacity: 0.25;
+}
+
+.SKIPPED > g > rect {
+ fill: #DDD;
+ stroke: #CCC;
+}
+
+.DISABLED {
+ opacity: 0.25;
+}
+
+.SKIPPED {
+ opacity: 0.25;
+}
+
+.QUEUED > g > rect {
+ fill: #39b3d7;
+ stroke: #39b3d7;
+}
+
+.QUEUED > g > text {
+ fill: #FFF;
+}
+
+.QUEUED {
+ opacity: 0.5;
+}
+
+/* Edges */
+.edge {
+ stroke: #CCC;
+ stroke-width: 1.5;
+
+ &:hover {
+ stroke: #009FC9;
+ stroke-width: 1.5;
+ }
+}
+
src/main/less/base.less 160(+160 -0)
diff --git a/src/main/less/base.less b/src/main/less/base.less
new file mode 100644
index 0000000..62861ca
--- /dev/null
+++ b/src/main/less/base.less
@@ -0,0 +1,160 @@
+.container-full {
+ padding: 0 105px;
+ margin: 0 auto;
+ width: 100%;
+ max-width: none;
+ min-width: 1075px;
+}
+
+.container-fill {
+ position: absolute;
+ top: 230px;
+ bottom: 50px;
+
+ .row {
+ height: 100%;
+ }
+
+ .col-sidebar {
+ height: 100%;
+ }
+
+ .col-content {
+ height: 100%;
+ }
+}
+
+.alert-default {
+ color: #a0a0a0;
+ background-color: #f5f5f5;
+ border-color: #dddddd;
+
+ hr {
+ border-top-color: #cccccc;
+ }
+
+ .alert-link {
+ color: #a0a0a0;
+ }
+}
+
+// Wide modal used for certain panels such as executing flow panel.
+.modal-wide .modal-dialog {
+ width: 80%;
+}
+
+// Hide messaging alert by default.
+.alert-messaging {
+ display: none;
+}
+
+// Add additional space under navs.
+.nav-tabs, .nav-pills {
+ margin-bottom: 15px;
+}
+
+.panel-list {
+ border: 0;
+}
+
+.list-group-collapse {
+ margin: 0;
+ .list-group-item {
+ border-radius: 0;
+ border-left: 0;
+ border-right: 0;
+
+ &:first-child {
+ border-top: 0;
+ }
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ button {
+ margin-left: 3px;
+ }
+ }
+}
+
+@media (min-width: 768px) {
+ .form-horizontal .control-label-center {
+ text-align: center;
+ }
+}
+
+.well-clear {
+ background-color: transparent;
+}
+
+.nav {
+ .nav-button {
+ margin-left: 5px;
+ }
+}
+
+.state-icon {
+ background-image: url("../css/images/ui-icons_cccccc_256x240.png");
+ cursor: pointer;
+ display: block;
+ float: left;
+ height: 16px;
+ width: 16px;
+ margin-right: 5px;
+
+ &.state-icon-expand {
+ background-position: -32px -16px;
+ }
+
+ &.state-icon-collapse {
+ background-position: -64px -16px;
+ }
+
+ &.state-icon-wait {
+ background-position: -64px -80px;
+ }
+}
+
+.editable {
+ margin: 0px;
+ cursor: pointer;
+ &:hover {
+ background-color: #fcfcfc;
+ }
+ &.editable-placeholder {
+ color: #a0a0a0;
+ }
+}
+
+.editable-form {
+ display: none;
+}
+
+.nav.nav-sm > li > a {
+ padding: 8px 12px;
+ font-size: 13px;
+}
+
+.scrollable {
+ padding: 0;
+ overflow: auto;
+ margin-bottom: 20px;
+
+ table {
+ margin-bottom: 0;
+ }
+}
+
+.panel-scrollable {
+ padding: 0;
+ overflow: auto;
+
+ table {
+ margin-bottom: 0;
+ }
+}
+
+.form-control-auto {
+ width: auto;
+}
src/main/less/flow.less 342(+342 -0)
diff --git a/src/main/less/flow.less b/src/main/less/flow.less
new file mode 100644
index 0000000..8ee150a
--- /dev/null
+++ b/src/main/less/flow.less
@@ -0,0 +1,342 @@
+#svgDiv {
+ height: 100%;
+ padding: 0px;
+}
+
+#graphView {
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+#flow-graph {
+ width: 100%;
+ height: 100%;
+}
+
+#headertabs {
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+#flow-executing-graph {
+ width: 100%;
+ height: 500px;
+}
+
+.flow-progress {
+ width: 280px;
+ margin: 4px;
+ background-color: #f5f5f5;
+ height: 24px;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.flow-progress-bar {
+ height: 100%;
+ background-color: #ccc;
+ border-radius: 5px;
+ -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ -webkit-transition: width 0.6s ease;
+ transition: width 0.6s ease;
+
+ &.attempt {
+ opacity: 0.70;
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ &.SUCCEEDED {
+ background-color: @flow-succeeded-color;
+ }
+
+ &.FAILED {
+ background-color: @flow-failed-color;
+ }
+
+ &.KILLED {
+ background-color: @flow-killed-color;
+ }
+
+ &.RUNNING {
+ background-color: @flow-running-color;
+ background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 40px 40px;
+ -webkit-animation: progress-bar-stripes 2s linear infinite;
+ animation: progress-bar-stripes 2s linear infinite;
+ }
+
+ &.QUEUED {
+ background-color: @flow-queued-color;
+ }
+
+ &.CANCELLED {
+ background-color: @flow-cancelled-color;
+ }
+}
+
+td {
+ > .listExpand {
+ width: 16px;
+ height: 16px;
+ float:right;
+ margin-top: 5px;
+ font-size: 8pt;
+ }
+
+ .status {
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+
+ padding: 2px 2px;
+ color: #FFF;
+ text-align: center;
+ margin-top: 2px;
+
+ &.SUCCEEDED {
+ background-color: @flow-succeeded-color;
+ }
+
+ &.FAILED {
+ background-color: @flow-failed-color;
+ }
+
+ &.KILLED {
+ background-color: @flow-killed-color;
+ }
+
+ &.PAUSED {
+ background-color: @flow-paused-color;
+ }
+
+ &.READY,
+ &.UNKNOWN,
+ &.PREPARING {
+ background-color: @flow-default-color;
+ }
+
+ &.RUNNING {
+ background-color: @flow-running-color;
+ }
+
+ &.FAILED_FINISHING {
+ background-color: @flow-failed-finishing-color;
+ }
+
+ &.DISABLED,
+ &.SKIPPED {
+ background-color: @flow-disabled-color;
+ }
+
+ &.CANCELLED {
+ background-color: @flow-cancelled-color;
+ }
+ }
+}
+
+#flowStatus {
+ &.SKIPPED {
+ color: @flow-disabled-color;
+ }
+
+ &.SUCCEEDED {
+ color: @flow-succeeded-color;
+ }
+
+ &.RUNNING {
+ color: @flow-running-color;
+ }
+
+ &.PAUSED {
+ color: @flow-paused-color;
+ }
+
+ &.FAILED {
+ color: @flow-failed-color;
+ }
+
+ &.KILLED {
+ color: @flow-killed-color;
+ }
+
+ &.CANCELLED {
+ color: @flow-cancelled-color;
+ }
+
+ &.FAILED_FINISHING {
+ color: @flow-failed-finishing-color;
+ }
+}
+
+.graph-sidebar {
+ height: 100%;
+ overflow-y: auto;
+
+ .graph-sidebar-list {
+ height: 100%;
+ }
+}
+
+.graph-sidebar-float {
+ position: absolute;
+ top: 0px;
+ bottom: 0px;
+
+ .graph-sidebar-list {
+ overflow-y: auto;
+ height: calc(~"100% - 102px");
+ }
+
+ .panel {
+ height: 100%;
+
+ .panel-heading {
+ padding-right: 10px;
+ }
+ }
+}
+
+.graph-container {
+ height: 100%;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.graph-sidebar-search {
+ width: 206px;
+ margin: 0px;
+}
+
+.graph-sidebar-close {
+ float: right;
+ color: #CCC;
+ padding: 5px 0px;
+ cursor: pointer;
+
+ &:hover {
+ color: #666;
+ }
+}
+
+.graph-sidebar-open {
+ position: absolute;
+ margin: 10px;
+ color: #CCC;
+ cursor: pointer;
+
+ &:hover {
+ color: #666;
+ }
+}
+
+ul.tree-list {
+ list-style-type: none;
+ padding-left: 0px;
+ margin: 0;
+}
+
+li.tree-list-item {
+ &.active > a {
+ background-color: #D9EDFF;
+ }
+
+ ul.tree-list {
+ padding-left: 20px;
+ }
+
+ &.subFilter > a > .expandarrow {
+ color : #f19153;
+ }
+
+ > a {
+ clear: both;
+ position: relative;
+ display: block;
+ border-bottom-width: 0;
+ padding: 5px 15px;
+ font-size: 10pt;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ background-color: #f5f5f5;
+ cursor: pointer;
+ }
+
+ &.nodedisabled,
+ &.DISABLED {
+ opacity: 0.3;
+ }
+
+ &.DISABLED .icon {
+ background-position: 16px 0px;
+ }
+
+ &.READY .icon {
+ background-position: 16px 0px;
+ }
+
+ &.QUEUED .icon {
+ opacity: 0.5;
+ background-position: 32px 0px;
+ }
+
+ &.RUNNING .icon {
+ background-position: 32px 0px;
+ }
+
+ &.SUCCEEDED .icon {
+ background-position: 48px 0px;
+ }
+
+ &.FAILED .icon {
+ background-position: 0px 0px;
+ }
+
+ &.KILLED .icon {
+ background-position: 0px 0px;
+ }
+
+ &.CANCELLED .icon {
+ background-position: 0px 0px;
+ opacity: 0.5;
+ }
+
+ &.FAILED_FINISHING .icon {
+ background-position: 0px 0px;
+ }
+
+ .icon {
+ float: left;
+ width: 16px;
+ height: 16px;
+ margin: 2px 4px 0px -5px;
+ background-image: url("./images/dot-icon.png");
+ background-position: 16px 0px;
+ }
+
+ .expandarrow {
+ float: right;
+ width: 16px;
+ height: 16px;
+ font-size: 8pt;
+ }
+
+ .filterHighlight {
+ background-color: #FFFF00;
+ }
+ }
+}
src/main/less/navbar.less 121(+121 -0)
diff --git a/src/main/less/navbar.less b/src/main/less/navbar.less
new file mode 100644
index 0000000..3826082
--- /dev/null
+++ b/src/main/less/navbar.less
@@ -0,0 +1,121 @@
+.navbar-logo {
+ float: left;
+ padding: 15px 15px;
+ font-size: 18px;
+ line-height: 20px;
+
+ a {
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+ }
+}
+
+@media (min-width: 768px) {
+ .navbar > .container .navbar-logo {
+ margin-left: -15px;
+ }
+}
+
+@media (min-width: 768px) {
+ .navbar-enviro {
+ width: auto;
+ }
+}
+
+.navbar-enviro {
+ margin: 30px 20px 0px 12px;
+
+ .navbar-enviro-name {
+ color: #ff3601;
+ font-family: Helvetica, Arial, Sans-Serif;
+ font-size: 118.75%;
+ font-weight: bold;
+ line-height: 100%;
+ }
+
+ .navbar-enviro-server {
+ color: #999;
+ font-family: Helvetica, Arial, Sans-Serif;
+ font-size: 75%;
+ }
+}
+
+// Restyle navbar-inverse for Azkaban navbar.
+.navbar-inverse {
+ background-color: #383838;
+ border-top: 5px solid #ff3601;
+ margin-bottom: 0;
+
+ .navbar-logo {
+ background: url('../../images/logo.png') top left no-repeat;
+ color: #ffffff;
+ font-size: 156.25%;
+ font-weight: bold;
+ margin: 15px 0.6% 15px 4.75%;
+ padding: 12px 0 11px 42px;
+
+ a {
+ color: #ffffff;
+ &:hover,
+ &:focus {
+ color: #ffffff;
+ background-color: transparent;
+ }
+ }
+ }
+
+ .navbar-nav {
+ font-family: Arial;
+ font-size: 81.25%;
+
+ > li {
+ padding: 25px 12px 25px 12px;
+ cursor: pointer;
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ }
+
+ > li > a {
+ padding: 0px;
+ color: #ccc;
+ &:focus,
+ &:hover {
+ color: #ccc;
+ background-color: transparent;
+ }
+ }
+
+ > .active {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+
+ > .active > a {
+ color: #fff;
+ font-weight: bold;
+ background-color: transparent;
+ border-bottom: 1px solid #ff3601;
+ &:hover {
+ color: #fff;
+ background-color: transparent;
+ }
+ }
+
+ > li.dropdown {
+ padding: 0;
+ }
+
+ > li.dropdown > a {
+ padding: 25px 12px 25px 12px;
+ }
+
+ > .open > a,
+ > .open > a:hover,
+ > .open > a:focus {
+ color: #ccc;
+ background-color: transparent;
+ }
+ }
+}
src/main/less/non-responsive.less 88(+88 -0)
diff --git a/src/main/less/non-responsive.less b/src/main/less/non-responsive.less
new file mode 100644
index 0000000..7fa282c
--- /dev/null
+++ b/src/main/less/non-responsive.less
@@ -0,0 +1,88 @@
+/* Non-responsive overrides
+ *
+ * Utilitze the following CSS to disable the responsive-ness of the container,
+ * grid system, and navbar.
+ */
+
+/* Reset the container */
+.container {
+ max-width: none !important;
+ width: 970px;
+}
+
+.container .navbar-header,
+.container .navbar-collapse {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+/* Always float the navbar header */
+.navbar-header {
+ float: left;
+}
+
+/* Undo the collapsing navbar */
+.navbar-collapse {
+ display: block !important;
+ height: auto !important;
+ padding-bottom: 0;
+ overflow: visible !important;
+}
+
+.navbar-toggle {
+ display: none;
+}
+.navbar-collapse {
+ border-top: 0;
+}
+
+.navbar-brand {
+ margin-left: -15px;
+}
+
+/* Always apply the floated nav */
+.navbar-nav {
+ float: left;
+ margin: 0;
+}
+.navbar-nav > li {
+ float: left;
+}
+.navbar-nav > li > a {
+ padding: 15px;
+}
+
+/* Redeclare since we override the float above */
+.navbar-nav.navbar-right {
+ float: right;
+}
+
+/* Undo custom dropdowns */
+.navbar .navbar-nav .open .dropdown-menu {
+ position: absolute;
+ float: left;
+ background-color: #fff;
+ border: 1px solid #cccccc;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-width: 0 1px 1px;
+ border-radius: 0 0 4px 4px;
+ -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+}
+.navbar-default .navbar-nav .open .dropdown-menu > li > a {
+ color: #333;
+}
+.navbar .navbar-nav .open .dropdown-menu > li > a:hover,
+.navbar .navbar-nav .open .dropdown-menu > li > a:focus,
+.navbar .navbar-nav .open .dropdown-menu > .active > a,
+.navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
+.navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
+ color: #fff !important;
+ background-color: #428bca !important;
+}
+.navbar .navbar-nav .open .dropdown-menu > .disabled > a,
+.navbar .navbar-nav .open .dropdown-menu > .disabled > a:hover,
+.navbar .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+ color: #999 !important;
+ background-color: transparent !important;
+}
src/main/less/project.less 142(+142 -0)
diff --git a/src/main/less/project.less b/src/main/less/project.less
new file mode 100644
index 0000000..0fb63af
--- /dev/null
+++ b/src/main/less/project.less
@@ -0,0 +1,142 @@
+#project-list {
+ padding: 0;
+ margin: 0px 0px 40px 0px;
+
+ li {
+ list-style: none;
+ border-bottom: 1px solid #cccccc;
+ padding-top: 14px;
+ padding-bottom: 0px;
+ &:first-child {
+ border-top: 1px solid #cccccc;
+ }
+ }
+
+ .project-expander {
+ float: right;
+ cursor: pointer;
+ &:hover {
+ color: #2a6496;
+ }
+ }
+
+ .project-info {
+ float: left;
+ h4 {
+ margin-top: 0;
+ margin-bottom: 4px;
+ }
+
+ .project-description {
+ margin-bottom: 4px;
+ }
+
+ .project-last-modified {
+ color: #a0a0a0;
+ margin-bottom: 16px;
+ strong {
+ font-weight: normal;
+ color: #000000;
+ }
+ }
+ }
+}
+
+#project-sidebar {
+ h3 {
+ margin-bottom: 5px;
+ }
+}
+
+.project-flows {
+ display: none;
+ background-color: #f9f9f9;
+ padding: 10px 15px 10px 15px;
+
+ h5 {
+ margin-top: 5px;
+ }
+
+ .list-group {
+ margin-bottom: 10px;
+ }
+
+ .list-group-item {
+ background: transparent;
+ padding: 7px 12px 7px 12px;
+ }
+}
+
+// Flow panel heading.
+.flow-expander {
+ cursor: pointer;
+
+ .flow-expander-icon {
+ color: #9a9a9a;
+ margin-right: 5px;
+ }
+}
+
+.expanded-flow-job-list {
+ .list-group-item {
+ .job-buttons {
+ visibility: hidden;
+ }
+
+ &:hover {
+ background-color: #f5f5f5;
+
+ .job-buttons {
+ visibility: visible;
+ }
+ }
+ }
+
+ .dependency {
+ background-color: #f0f0f0;
+ }
+
+ .dependent {
+ background-color: #fafafa;
+ }
+}
+
+// Permissions page table.
+.permission-table {
+ .tb-perm {
+ width: 41px;
+ margin: 0px;
+ }
+
+ .tb-admin {
+ width: 41px;
+ margin: 0px;
+ }
+
+ .tb-read {
+ width: 33px;
+ margin: 0px;
+ }
+
+ .tb-write {
+ width: 34px;
+ margin: 0px;
+ }
+
+ .tb-execute {
+ width: 51px;
+ margin: 0px;
+ }
+
+ .tb-schedule {
+ margin: 0px;
+ width: 60px;
+ }
+
+ .tb-action {
+ margin: 0px;
+ width: 70px;
+ min-width: 70px;
+ max-width: 70px;
+ }
+}
src/main/less/tables.less 149(+149 -0)
diff --git a/src/main/less/tables.less b/src/main/less/tables.less
new file mode 100644
index 0000000..5d5aeea
--- /dev/null
+++ b/src/main/less/tables.less
@@ -0,0 +1,149 @@
+table.table-properties {
+ table-layout: fixed;
+ word-wrap: break-word;
+}
+
+// Flow summary.
+.property-key {
+ width: 25%;
+ font-weight: bold;
+}
+
+.property-value {
+
+}
+
+.property-value-half {
+ width: 25%;
+}
+
+.property-key,
+.property-value,
+.property-value-half {
+ pre {
+ background: transparent;
+ padding: 0;
+ border: 0;
+ }
+}
+
+.editable {
+ .remove-btn {
+ visibility: hidden;
+ }
+
+ &:hover .remove-btn {
+ visibility: visible;
+ }
+}
+
+// Job table.
+#all-jobs {
+ .tb-name {
+ width: 70%;
+ border-bottom-width: 0;
+ border-bottom-style: none;
+ }
+
+ .tb-up-date {
+ width: 140px;
+ min-width: 130px;
+ }
+
+ .tb-owner {
+ width: 10%;
+ min-width: 95px;
+ }
+}
+
+// Properties table.
+.properties-table {
+ .all-jobs .tb-pname {
+ }
+
+ .all-jobs .tb-pvalue {
+ }
+}
+
+// Table of executions.
+.executions-table {
+ tr {
+ &.expanded {
+ opacity: 0.6;
+ }
+ }
+
+ td {
+ &.subflowrow {
+ padding: 0px 0px;
+
+ table {
+ margin: 0px;
+ background-color: rgba(230, 230, 230, 0.75);
+
+ td {
+ background-color: none;
+ }
+ }
+ }
+
+ &.date {
+ width: 160px;
+ }
+
+ &.jobtype {
+ width: 90px;
+ }
+
+ &.execid {
+ width: 100px;
+ }
+
+ &.project {
+ width: 200px;
+ }
+
+ &.user {
+ width: 60px;
+ }
+
+ &.elapse {
+ width: 90px;
+ }
+
+ &.statustd {
+ width: 100px;
+ }
+
+ &.details {
+ width: 10px;
+ }
+
+ &.action {
+ width: 20px;
+ }
+
+ &.logs {
+ width: 30px;
+ }
+
+ &.timeline {
+ width: 280px;
+ padding: 0px 0px 0px 4px;
+ height: 100%;
+ vertical-align: bottom;
+ margin: 0px;
+ }
+
+ &.startTime {
+ width: 160px;
+ }
+
+ &.endTime {
+ width: 160px;
+ }
+ &.elapsedTime {
+ width: 90px;
+ }
+ }
+}
diff --git a/src/main/resources/azkaban/webapp/servlet/velocity/triggerspage.vm b/src/main/resources/azkaban/webapp/servlet/velocity/triggerspage.vm
new file mode 100644
index 0000000..c48ec9e
--- /dev/null
+++ b/src/main/resources/azkaban/webapp/servlet/velocity/triggerspage.vm
@@ -0,0 +1,95 @@
+#*
+ * Copyright 2012 LinkedIn, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+*#
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+#parse("azkaban/webapp/servlet/velocity/style.vm")
+#parse("azkaban/webapp/servlet/velocity/javascript.vm")
+
+ <link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
+ <link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui.css" />
+
+ <script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-1.10.1.custom.js"></script>
+ <script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-timepicker-addon.js"></script>
+ <script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-sliderAccess.js"></script>
+
+ <script type="text/javascript" src="${context}/js/azkaban/view/table-sort.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/triggers.js"></script>
+ <script type="text/javascript">
+ var contextURL = "${context}";
+ var currentTime = ${currentTime};
+ var timezone = "${timezone}";
+ var errorMessage = null;
+ var successMessage = null;
+ </script>
+ </head>
+ <body>
+
+#set ($current_page="triggers")
+#parse ("azkaban/webapp/servlet/velocity/nav.vm")
+
+#if ($errorMsg)
+ #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
+#else
+
+ <div class="az-page-header">
+ <div class="container-full">
+ <h1>All Triggers</h1>
+ </div>
+ </div>
+
+ <div class="container-full">
+
+ #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
+
+ <div class="row">
+ <div class="col-xs-12">
+ <table id="triggersTbl" class="table table-striped table-bordered table-condensed table-hover">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Source</th>
+ <th>Submitted By</th>
+ <th>Description</th>
+ <th>Status</th>
+ <!--th colspan="2" class="action ignoresort">Action</th-->
+ </tr>
+ </thead>
+ <tbody>
+ #if ($triggers)
+ #foreach ($trigger in $triggers)
+ <tr>
+ <td>${trigger.triggerId}</td>
+ <td>${trigger.source}</td>
+ <td>${trigger.submitUser}</td>
+ <td>${trigger.getDescription()}</td>
+ <td>${trigger.getStatus()}</td>
+ <!--td><button id="expireTriggerBtn" onclick="expireTrigger(${trigger.triggerId})" >Expire Trigger</button></td-->
+ </tr>
+ #end
+ #else
+ <tr><td class="last">No Trigger Found</td></tr>
+ #end
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+#end
+ </body>
+</html>
src/main/tl/.gitignore 1(+1 -0)
diff --git a/src/main/tl/.gitignore b/src/main/tl/.gitignore
new file mode 100644
index 0000000..2416a67
--- /dev/null
+++ b/src/main/tl/.gitignore
@@ -0,0 +1 @@
+obj/
src/main/tl/flowstats.tl 155(+155 -0)
diff --git a/src/main/tl/flowstats.tl b/src/main/tl/flowstats.tl
new file mode 100644
index 0000000..5276ce8
--- /dev/null
+++ b/src/main/tl/flowstats.tl
@@ -0,0 +1,155 @@
+ {?histogram}
+ <div class="row">
+ <div class="col-xs-12">
+ <div class="well well-clear well-sm">
+ <div id="job-histogram"></div>
+ </div>
+ </div>
+ </div>
+ {/histogram}
+
+ {?warnings}
+ <div class="alert alert-warning">
+ <h4>Warnings</h4>
+ <p>These stats may have reduced accuracy due to the following missing information:</p>
+ <ul>
+ {#warnings}
+ <li>{.}</li>
+ {/warnings}
+ </ul>
+ </div>
+ {/warnings}
+
+ <div class="row">
+ <div class="col-xs-12">
+ <h4>Resources</h4>
+ <table class="table table-bordered table-condensed table-striped">
+ <thead>
+ <tr>
+ <th class="property-key">Resource</th>
+ <th class="property-key">Value</th>
+ <th>Job Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="property-key">Max Map Slots</td>
+ <td>{stats.mapSlots.max}</td>
+ <td>{stats.mapSlots.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max Reduce Slots</td>
+ <td>{stats.reduceSlots.max}</td>
+ <td>{stats.reduceSlots.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Total Map Slots</td>
+ <td colspan="2">{stats.totalMapSlots}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Total Reduce Slots</td>
+ <td colspan="2">{stats.totalReduceSlots}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-xs-12">
+ <h4>Parameters</h4>
+ <table class="table table-bordered table-condensed table-striped">
+ <thead>
+ <tr>
+ <th class="property-key">Parameter</th>
+ <th class="property-key">Value</th>
+ <th>Job Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="property-key">Max <code>-Xmx</code></td>
+ <td>{stats.xmx.str}</td>
+ <td>{stats.xmx.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max <code>-Xms</code></td>
+ {?stats.xms.set}
+ <td>
+ {stats.xms.str}
+ </td>
+ <td>
+ {stats.xms.job}
+ </td>
+ {:else}
+ <td colspan="2">
+ Not set.
+ </td>
+ {/stats.xms.set}
+ </tr>
+ <tr>
+ <td class="property-key">Max <code>mapred.job.map.memory.mb</code></td>
+ <td>{stats.jobMapMemoryMb.max}</td>
+ <td>{stats.jobMapMemoryMb.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max <code>mapred.job.reduce.memory.mb</code></td>
+ <td>{stats.jobReduceMemoryMb.max}</td>
+ <td>{stats.jobReduceMemoryMb.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max Distributed Cache</td>
+ {?stats.distributedCache.using}
+ <td>
+ {stats.distributedCache.max}
+ </td>
+ <td>
+ {stats.distributedCache.job}
+ </td>
+ {:else}
+ <td colspan="2">
+ Not used.
+ </td>
+ {/stats.distributedCache.using}
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-xs-12">
+ <h4>Counters</h4>
+ <table class="table table-bordered table-condensed">
+ <thead>
+ <tr>
+ <th class="property-key">Parameter</th>
+ <th class="property-key">Value</th>
+ <th>Job Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="property-key">Max <code>FILE_BYTES_READ</code></td>
+ <td>{stats.fileBytesRead.max}</td>
+ <td>{stats.fileBytesRead.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max <code>HDFS_BYTES_READ</code></td>
+ <td>{stats.hdfsBytesRead.max}</td>
+ <td>{stats.hdfsBytesRead.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max <code>FILE_BYTES_WRITTEN</code></td>
+ <td>{stats.fileBytesWritten.max}</td>
+ <td>{stats.fileBytesWritten.job}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Max <code>HDFS_BYTES_WRITTEN</code></td>
+ <td>{stats.hdfsBytesWritten.max}</td>
+ <td>{stats.hdfsBytesWritten.job}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
src/main/tl/flowsummary.tl 69(+69 -0)
diff --git a/src/main/tl/flowsummary.tl b/src/main/tl/flowsummary.tl
new file mode 100644
index 0000000..82627d6
--- /dev/null
+++ b/src/main/tl/flowsummary.tl
@@ -0,0 +1,69 @@
+ <div class="row">
+ <div class="col-xs-12">
+ <table class="table table-bordered table-condensed">
+ <tbody>
+ <tr>
+ <td class="property-key">Project name</td>
+ <td>{projectName}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Job Types Used</td>
+ <td>{#jobTypes}{.} {/jobTypes}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-xs-12">
+ <h3>
+ Scheduling
+ {?schedule}
+ <div class="pull-right">
+ <button type="button" id="removeSchedBtn" class="btn btn-sm btn-danger" onclick="removeSched({schedule.scheduleId})" >Remove Schedule</button>
+ </div>
+ {/schedule}
+ </h3>
+ {?schedule}
+ <table class="table table-condensed table-bordered">
+ <tbody>
+ <tr>
+ <td class="property-key">Schedule ID</td>
+ <td class="property-value-half">{schedule.scheduleId}</td>
+ <td class="property-key">Submitted By</td>
+ <td class="property-value-half">{schedule.submitUser}</td>
+ </tr>
+ <tr>
+ <td class="property-key">First Scheduled to Run</td>
+ <td class="property-value-half">{schedule.firstSchedTime}</td>
+ <td class="property-key">Repeats Every</td>
+ <td class="property-value-half">{schedule.period}</td>
+ </tr>
+ <tr>
+ <td class="property-key">Next Execution Time</td>
+ <td class="property-value-half">{schedule.nextExecTime}</td>
+ <td class="property-key">SLA</td>
+ <td class="property-value-half">
+ {?schedule.slaOptions}
+ true
+ {:else}
+ false
+ {/schedule.slaOptions}
+ <div class="pull-right">
+ <button type="button" id="addSlaBtn" class="btn btn-xs btn-primary" onclick="slaView.initFromSched({schedule.scheduleId}, '{flowName}')" >View/Set SLA</button>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ {:else}
+ <div class="callout callout-default">
+ <h4>None</h4>
+ <p>This flow has not been scheduled.</p>
+ </div>
+ {/schedule}
+
+ <h3>Last Run Stats</h3>
+ </div>
+ </div>
diff --git a/unit/java/azkaban/test/execapp/FlowRunnerPipelineTest.java b/unit/java/azkaban/test/execapp/FlowRunnerPipelineTest.java
index 9df97ac..aa1eee5 100644
--- a/unit/java/azkaban/test/execapp/FlowRunnerPipelineTest.java
+++ b/unit/java/azkaban/test/execapp/FlowRunnerPipelineTest.java
@@ -5,10 +5,9 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
-import junit.framework.Assert;
-
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
+import org.junit.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/unit/java/azkaban/test/execapp/FlowRunnerTest2.java b/unit/java/azkaban/test/execapp/FlowRunnerTest2.java
index 9605c12..7a67da6 100644
--- a/unit/java/azkaban/test/execapp/FlowRunnerTest2.java
+++ b/unit/java/azkaban/test/execapp/FlowRunnerTest2.java
@@ -5,10 +5,9 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
-import junit.framework.Assert;
-
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
+import org.junit.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/unit/java/azkaban/test/executor/ExecutableFlowTest.java b/unit/java/azkaban/test/executor/ExecutableFlowTest.java
index 9f2a3c2..6e4b0b1 100644
--- a/unit/java/azkaban/test/executor/ExecutableFlowTest.java
+++ b/unit/java/azkaban/test/executor/ExecutableFlowTest.java
@@ -8,9 +8,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
-import junit.framework.Assert;
-
import org.apache.log4j.Logger;
+import org.junit.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
diff --git a/unit/java/azkaban/test/utils/DirectoryFlowLoaderTest.java b/unit/java/azkaban/test/utils/DirectoryFlowLoaderTest.java
index d671e6f..800df48 100644
--- a/unit/java/azkaban/test/utils/DirectoryFlowLoaderTest.java
+++ b/unit/java/azkaban/test/utils/DirectoryFlowLoaderTest.java
@@ -2,9 +2,8 @@ package azkaban.test.utils;
import java.io.File;
-import junit.framework.Assert;
-
import org.apache.log4j.Logger;
+import org.junit.Assert;
import org.junit.Test;
import azkaban.utils.DirectoryFlowLoader;