azkaban-developers

Moving all files to conform to gradle conventions. The reason

3/19/2014 1:38:40 AM

Changes

build.gradle 154(+113 -41)

src/less/.gitignore 1(+0 -1)

src/less/azkaban-graph.less 165(+0 -165)

src/less/base.less 160(+0 -160)

src/less/flow.less 342(+0 -342)

src/less/navbar.less 121(+0 -121)

src/less/non-responsive.less 88(+0 -88)

src/less/project.less 142(+0 -142)

src/less/tables.less 149(+0 -149)

src/tl/.gitignore 1(+0 -1)

src/tl/flowstats.tl 155(+0 -155)

src/tl/flowsummary.tl 69(+0 -69)

Details

build.gradle 154(+113 -41)

diff --git a/build.gradle b/build.gradle
index 22ef51b..0ea523c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,6 +22,7 @@ def getVersionName = { ->
     return cmdCaller(['git', 'describe', '--tags', '--abbrev=0'])
 }
 
+
 version = getVersionName()
 archivesBaseName = 'azkaban'
 check.dependsOn.remove(test)
@@ -109,30 +110,11 @@ eclipse.classpath.file {
     }
 }
 
-sourceSets {
-  main {
-    java {
-      srcDir 'src/java'
-    }
-    resources {
-      // Used to include all vm's and property files in the src code
-      srcDir 'src/java'
-      include '**/*.vm'
-      include '**/*.properties'
-    }
-  }
-  test {
-    java {
-      srcDir 'unit/java'
-    }
-  }
-}
-
 /**
  * Invokes a makefile target that will compile less files
  */
 task compileLess(type:Exec) {
-    workingDir 'src/less'
+    workingDir 'src/main/less'
     commandLine 'make', '-e'
     environment (
       OBJ_DIR : file(new File(buildDir,'/less'))
@@ -143,7 +125,7 @@ task compileLess(type:Exec) {
  * Invokes a makefile target that will compile dust files
  */
 task compileDust(type:Exec) {
-    workingDir 'src/tl'
+    workingDir 'src/main/tl'
     commandLine 'make', '-e'
     environment (
       OBJ_DIR : file(new File(buildDir,'/dust'))
@@ -185,13 +167,14 @@ task createVersionFile() << {
                         formattedDate + '\n'
 
     File versionFile = file('build/package/version.file')
+    versionFile.parentFile.mkdirs()
     versionFile.write(versionStr)
-    
 }
 
-task packageSolo(dependsOn: [jar, 'web', 'createVersionFile']) << {
-    String packageDir = 'build/package/azkaban-solo-server'
- 
+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'
@@ -205,13 +188,13 @@ task packageSolo(dependsOn: [jar, 'web', 'createVersionFile']) << {
     
     println 'Copying Azkaban lib'
     copy {
-        from('$buildDir/libs')
+        from('build/libs')
         into(packageDir + '/lib')
     }
     
     println 'Copying web'
     copy {
-        from('$buildDir/web')
+        from('build/web')
         into(packageDir + '/web')
     }
 
@@ -231,10 +214,63 @@ task packageSolo(dependsOn: [jar, 'web', 'createVersionFile']) << {
         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
+    }
 } 
 
-task packageExec(dependsOn: [jar, 'web', 'createVersionFile']) << {
-    String packageDir = 'build/package/azkaban-exec-server'
+task packageSql(type: Tar) {
+    String packageDir = 'build/package/sql'
+ 
+    println 'Creating Azkaban Solo Server Package 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
+    }
+} 
+
+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
@@ -249,15 +285,9 @@ task packageExec(dependsOn: [jar, 'web', 'createVersionFile']) << {
     
     println 'Copying Azkaban lib'
     copy {
-        from('$buildDir/libs')
+        from('build/libs')
         into(packageDir + '/lib')
     }
-    
-    println 'Copying web'
-    copy {
-        from('$buildDir/web')
-        into(packageDir + '/web')
-    }
 
     println 'Copying dependency jars'
     copy {
@@ -269,10 +299,31 @@ task packageExec(dependsOn: [jar, 'web', 'createVersionFile']) << {
         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
+    }
 }
 
-task packageWeb(dependsOn: [jar, 'web', 'createVersionFile']) << {
-    String packageDir = 'build/package/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
@@ -287,13 +338,13 @@ task packageWeb(dependsOn: [jar, 'web', 'createVersionFile']) << {
     
     println 'Copying Azkaban lib'
     copy {
-        from('$buildDir/libs')
+        from('build/libs')
         into(packageDir + '/lib')
     }
     
     println 'Copying web'
     copy {
-        from('$buildDir/web')
+        from('build/web')
         into(packageDir + '/web')
     }
 
@@ -307,4 +358,25 @@ task packageWeb(dependsOn: [jar, 'web', 'createVersionFile']) << {
         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
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/
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;
+  }
+}
+
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;
+}
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;
+    }
+  }
+}
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;
+    }
+  }
+}
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;
+}
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;
+  }
+}
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/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/
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>
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>