azkaban-aplcache
Changes
.gitignore 7(+1 -6)
.travis.yml 4(+1 -3)
azkaban-common/.gitignore 1(+1 -0)
azkaban-common/src/main/java/azkaban/jobExecutor/utils/process/ProcessFailureException.java 0(+0 -0)
azkaban-execserver/.gitignore 2(+2 -0)
azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/invalidsessionmodal.vm 0(+0 -0)
azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm 78(+78 -0)
azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm 117(+117 -0)
azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduleoptionspanel.vm 175(+175 -0)
azkaban-webserver/src/main/tl/flowstats.tl 155(+155 -0)
azkaban-webserver/src/main/tl/flowsummary.tl 69(+69 -0)
azkaban-webserver/src/web/css/jquery.svg.css 15(+15 -0)
azkaban-webserver/src/web/css/morris.css 25(+25 -0)
azkaban-webserver/src/web/js/azkaban/util/ajax.js 206(+206 -0)
azkaban-webserver/src/web/js/azkaban/util/layout.js 384(+384 -0)
azkaban-webserver/src/web/js/azkaban/view/flow.js 515(+515 -0)
azkaban-webserver/src/web/js/azkaban/view/jmx.js 152(+152 -0)
azkaban-webserver/src/web/js/azkaban/view/main.js 209(+209 -0)
azkaban-webserver/src/web/js/azkaban/view/project.js 244(+244 -0)
azkaban-webserver/src/web/js/jquery.svgdom.js 406(+406 -0)
build.gradle 764(+388 -376)
gradle.properties 2(+2 -0)
gradle/wrapper/gradle-wrapper.jar 0(+0 -0)
README.md 27(+22 -5)
settings.gradle 6(+6 -0)
Details
.gitignore 7(+1 -6)
diff --git a/.gitignore b/.gitignore
index 553b1a2..a55a10d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,9 @@
-dist/
build/
.gradle/
.settings/
-node_modules/
.idea/
*.iml
-*.log
-TestProcess_*
-_AzkabanTestDir_*
-reports/
/bin
.classpath
.project
+*.swp
.travis.yml 4(+1 -3)
diff --git a/.travis.yml b/.travis.yml
index 4f1c17c..09aad73 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,2 @@
languages: java
-install:
- - time npm install -g less dustjs-linkedin
-script: ./gradlew dist
+script: ./gradlew distTar
azkaban-common/.gitignore 1(+1 -0)
diff --git a/azkaban-common/.gitignore b/azkaban-common/.gitignore
new file mode 100644
index 0000000..bbcdd31
--- /dev/null
+++ b/azkaban-common/.gitignore
@@ -0,0 +1 @@
+TestProcess*
diff --git a/azkaban-common/src/main/java/azkaban/server/ServerConstants.java b/azkaban-common/src/main/java/azkaban/server/ServerConstants.java
new file mode 100644
index 0000000..a348364
--- /dev/null
+++ b/azkaban-common/src/main/java/azkaban/server/ServerConstants.java
@@ -0,0 +1,21 @@
+/*
+ * 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.server;
+
+public class ServerConstants {
+ public static final String AZKABAN_SERVLET_CONTEXT_KEY = "azkaban_app";
+}
diff --git a/azkaban-common/src/test/java/azkaban/database/AzkabanDatabaseSetupTest.java b/azkaban-common/src/test/java/azkaban/database/AzkabanDatabaseSetupTest.java
new file mode 100644
index 0000000..a3b18cc
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/database/AzkabanDatabaseSetupTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2014 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.database;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.io.FileUtils;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.utils.Props;
+
+@Ignore
+public class AzkabanDatabaseSetupTest {
+ @BeforeClass
+ public static void setupDB() throws IOException, SQLException {
+ File dbDir = new File("h2dbtest");
+ if (dbDir.exists()) {
+ FileUtils.deleteDirectory(dbDir);
+ }
+
+ dbDir.mkdir();
+
+ clearUnitTestDB();
+ }
+
+ @AfterClass
+ public static void teardownDB() {
+ }
+
+ @Test
+ public void testH2Query() throws Exception {
+ Props h2Props = getH2Props();
+ AzkabanDatabaseSetup setup = new AzkabanDatabaseSetup(h2Props);
+
+ // First time will create the tables
+ setup.loadTableInfo();
+ setup.printUpgradePlan();
+ setup.updateDatabase(true, true);
+ Assert.assertTrue(setup.needsUpdating());
+
+ // Second time will update some tables. This is only for testing purpose and
+ // obviously we
+ // wouldn't set things up this way.
+ setup.loadTableInfo();
+ setup.printUpgradePlan();
+ setup.updateDatabase(true, true);
+ Assert.assertTrue(setup.needsUpdating());
+
+ // Nothing to be done
+ setup.loadTableInfo();
+ setup.printUpgradePlan();
+ Assert.assertFalse(setup.needsUpdating());
+ }
+
+ @Test
+ public void testMySQLQuery() throws Exception {
+ Props mysqlProps = getMySQLProps();
+ AzkabanDatabaseSetup setup = new AzkabanDatabaseSetup(mysqlProps);
+
+ // First time will create the tables
+ setup.loadTableInfo();
+ setup.printUpgradePlan();
+ setup.updateDatabase(true, true);
+ Assert.assertTrue(setup.needsUpdating());
+
+ // Second time will update some tables. This is only for testing purpose
+ // and obviously we wouldn't set things up this way.
+ setup.loadTableInfo();
+ setup.printUpgradePlan();
+ setup.updateDatabase(true, true);
+ Assert.assertTrue(setup.needsUpdating());
+
+ // Nothing to be done
+ setup.loadTableInfo();
+ setup.printUpgradePlan();
+ Assert.assertFalse(setup.needsUpdating());
+ }
+
+ private static Props getH2Props() {
+ Props props = new Props();
+ props.put("database.type", "h2");
+ props.put("h2.path", "h2dbtest/h2db");
+ props.put("database.sql.scripts.dir", "unit/sql");
+
+ return props;
+ }
+
+ private static Props getMySQLProps() {
+ Props props = new Props();
+
+ props.put("database.type", "mysql");
+ props.put("mysql.port", "3306");
+ props.put("mysql.host", "localhost");
+ props.put("mysql.database", "azkabanunittest");
+ props.put("mysql.user", "root");
+ props.put("database.sql.scripts.dir", "unit/sql");
+ props.put("mysql.password", "");
+ props.put("mysql.numconnections", 10);
+
+ return props;
+ }
+
+ private static void clearUnitTestDB() throws SQLException {
+ Props props = new Props();
+
+ props.put("database.type", "mysql");
+ props.put("mysql.host", "localhost");
+ props.put("mysql.port", "3306");
+ props.put("mysql.database", "");
+ props.put("mysql.user", "root");
+ props.put("mysql.password", "");
+ props.put("mysql.numconnections", 10);
+
+ DataSource datasource = DataSourceUtils.getDataSource(props);
+ QueryRunner runner = new QueryRunner(datasource);
+ try {
+ runner.update("drop database azkabanunittest");
+ } catch (SQLException e) {
+ }
+ runner.update("create database azkabanunittest");
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/database/AzkabanDatabaseUpdaterTest.java b/azkaban-common/src/test/java/azkaban/database/AzkabanDatabaseUpdaterTest.java
new file mode 100644
index 0000000..4efc11b
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/database/AzkabanDatabaseUpdaterTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014 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.database;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.io.FileUtils;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.utils.Props;
+
+@Ignore
+public class AzkabanDatabaseUpdaterTest {
+ @BeforeClass
+ public static void setupDB() throws IOException, SQLException {
+ File dbDir = new File("h2dbtest");
+ if (dbDir.exists()) {
+ FileUtils.deleteDirectory(dbDir);
+ }
+
+ dbDir.mkdir();
+
+ clearUnitTestDB();
+ }
+
+ @AfterClass
+ public static void teardownDB() {
+ }
+
+ @Test
+ public void testMySQLAutoCreate() throws Exception {
+ String confDir = "unit/conf/dbtestmysql";
+ System.out.println("1.***Now testing check");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir });
+
+ System.out.println("2.***Now testing update");
+ AzkabanDatabaseUpdater.main(new String[] { "-u", "-c", confDir });
+
+ System.out.println("3.***Now testing check again");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir });
+
+ System.out.println("4.***Now testing update again");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir, "-u" });
+
+ System.out.println("5.***Now testing check again");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir });
+ }
+
+ @Test
+ public void testH2AutoCreate() throws Exception {
+ String confDir = "unit/conf/dbtesth2";
+ System.out.println("1.***Now testing check");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir });
+
+ System.out.println("2.***Now testing update");
+ AzkabanDatabaseUpdater.main(new String[] { "-u", "-c", confDir });
+
+ System.out.println("3.***Now testing check again");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir });
+
+ System.out.println("4.***Now testing update again");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir, "-u" });
+
+ System.out.println("5.***Now testing check again");
+ AzkabanDatabaseUpdater.main(new String[] { "-c", confDir });
+ }
+
+ private static void clearUnitTestDB() throws SQLException {
+ Props props = new Props();
+
+ props.put("database.type", "mysql");
+ props.put("mysql.host", "localhost");
+ props.put("mysql.port", "3306");
+ props.put("mysql.database", "");
+ props.put("mysql.user", "root");
+ props.put("mysql.password", "");
+ props.put("mysql.numconnections", 10);
+
+ DataSource datasource = DataSourceUtils.getDataSource(props);
+ QueryRunner runner = new QueryRunner(datasource);
+ try {
+ runner.update("drop database azkabanunittest");
+ } catch (SQLException e) {
+ }
+ runner.update("create database azkabanunittest");
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/ExecutableFlowTest.java b/azkaban-common/src/test/java/azkaban/executor/ExecutableFlowTest.java
new file mode 100644
index 0000000..8997fee
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/ExecutableFlowTest.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2014 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.executor;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.log4j.Logger;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.executor.ExecutionOptions.FailureAction;
+import azkaban.flow.Flow;
+import azkaban.project.Project;
+import azkaban.utils.DirectoryFlowLoader;
+import azkaban.utils.JSONUtils;
+
+public class ExecutableFlowTest {
+ private Project project;
+
+ @Before
+ public void setUp() throws Exception {
+ Logger logger = Logger.getLogger(this.getClass());
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+ loader.loadProjectFlow(new File("unit/executions/embedded"));
+ Assert.assertEquals(0, loader.getErrors().size());
+
+ project = new Project(11, "myTestProject");
+ project.setFlows(loader.getFlowMap());
+ project.setVersion(123);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Ignore @Test
+ public void testExecutorFlowCreation() throws Exception {
+ Flow flow = project.getFlow("jobe");
+ Assert.assertNotNull(flow);
+
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+ Assert.assertNotNull(exFlow.getExecutableNode("joba"));
+ Assert.assertNotNull(exFlow.getExecutableNode("jobb"));
+ Assert.assertNotNull(exFlow.getExecutableNode("jobc"));
+ Assert.assertNotNull(exFlow.getExecutableNode("jobd"));
+ Assert.assertNotNull(exFlow.getExecutableNode("jobe"));
+
+ Assert.assertFalse(exFlow.getExecutableNode("joba") instanceof ExecutableFlowBase);
+ Assert.assertTrue(exFlow.getExecutableNode("jobb") instanceof ExecutableFlowBase);
+ Assert.assertTrue(exFlow.getExecutableNode("jobc") instanceof ExecutableFlowBase);
+ Assert.assertTrue(exFlow.getExecutableNode("jobd") instanceof ExecutableFlowBase);
+ Assert.assertFalse(exFlow.getExecutableNode("jobe") instanceof ExecutableFlowBase);
+
+ ExecutableFlowBase jobbFlow =
+ (ExecutableFlowBase) exFlow.getExecutableNode("jobb");
+ ExecutableFlowBase jobcFlow =
+ (ExecutableFlowBase) exFlow.getExecutableNode("jobc");
+ ExecutableFlowBase jobdFlow =
+ (ExecutableFlowBase) exFlow.getExecutableNode("jobd");
+
+ Assert.assertEquals("innerFlow", jobbFlow.getFlowId());
+ Assert.assertEquals("jobb", jobbFlow.getId());
+ Assert.assertEquals(4, jobbFlow.getExecutableNodes().size());
+
+ Assert.assertEquals("innerFlow", jobcFlow.getFlowId());
+ Assert.assertEquals("jobc", jobcFlow.getId());
+ Assert.assertEquals(4, jobcFlow.getExecutableNodes().size());
+
+ Assert.assertEquals("innerFlow", jobdFlow.getFlowId());
+ Assert.assertEquals("jobd", jobdFlow.getId());
+ Assert.assertEquals(4, jobdFlow.getExecutableNodes().size());
+ }
+
+ @Ignore @Test
+ public void testExecutorFlowJson() throws Exception {
+ Flow flow = project.getFlow("jobe");
+ Assert.assertNotNull(flow);
+
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+
+ Object obj = exFlow.toObject();
+ String exFlowJSON = JSONUtils.toJSON(obj);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> flowObjMap =
+ (Map<String, Object>) JSONUtils.parseJSONFromString(exFlowJSON);
+
+ ExecutableFlow parsedExFlow =
+ ExecutableFlow.createExecutableFlowFromObject(flowObjMap);
+ testEquals(exFlow, parsedExFlow);
+ }
+
+ @Ignore @Test
+ public void testExecutorFlowJson2() throws Exception {
+ Flow flow = project.getFlow("jobe");
+ Assert.assertNotNull(flow);
+
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+ exFlow.setExecutionId(101);
+ exFlow.setAttempt(2);
+ exFlow.setDelayedExecution(1000);
+
+ ExecutionOptions options = new ExecutionOptions();
+ options.setConcurrentOption("blah");
+ options.setDisabledJobs(Arrays.asList(new Object[] { "bee", null, "boo" }));
+ options.setFailureAction(FailureAction.CANCEL_ALL);
+ options
+ .setFailureEmails(Arrays.asList(new String[] { "doo", null, "daa" }));
+ options
+ .setSuccessEmails(Arrays.asList(new String[] { "dee", null, "dae" }));
+ options.setPipelineLevel(2);
+ options.setPipelineExecutionId(3);
+ options.setNotifyOnFirstFailure(true);
+ options.setNotifyOnLastFailure(true);
+
+ HashMap<String, String> flowProps = new HashMap<String, String>();
+ flowProps.put("la", "fa");
+ options.addAllFlowParameters(flowProps);
+ exFlow.setExecutionOptions(options);
+
+ Object obj = exFlow.toObject();
+ String exFlowJSON = JSONUtils.toJSON(obj);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> flowObjMap =
+ (Map<String, Object>) JSONUtils.parseJSONFromString(exFlowJSON);
+
+ ExecutableFlow parsedExFlow =
+ ExecutableFlow.createExecutableFlowFromObject(flowObjMap);
+ testEquals(exFlow, parsedExFlow);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Ignore @Test
+ public void testExecutorFlowUpdates() throws Exception {
+ Flow flow = project.getFlow("jobe");
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+ exFlow.setExecutionId(101);
+
+ // Create copy of flow
+ Object obj = exFlow.toObject();
+ String exFlowJSON = JSONUtils.toJSON(obj);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> flowObjMap =
+ (Map<String, Object>) JSONUtils.parseJSONFromString(exFlowJSON);
+ ExecutableFlow copyFlow =
+ ExecutableFlow.createExecutableFlowFromObject(flowObjMap);
+
+ testEquals(exFlow, copyFlow);
+
+ ExecutableNode joba = exFlow.getExecutableNode("joba");
+ ExecutableFlowBase jobb =
+ (ExecutableFlowBase) (exFlow.getExecutableNode("jobb"));
+ ExecutableFlowBase jobc =
+ (ExecutableFlowBase) (exFlow.getExecutableNode("jobc"));
+ ExecutableFlowBase jobd =
+ (ExecutableFlowBase) (exFlow.getExecutableNode("jobd"));
+ ExecutableNode jobe = exFlow.getExecutableNode("jobe");
+ assertNotNull(joba, jobb, jobc, jobd, jobe);
+
+ ExecutableNode jobbInnerFlowA = jobb.getExecutableNode("innerJobA");
+ ExecutableNode jobbInnerFlowB = jobb.getExecutableNode("innerJobB");
+ ExecutableNode jobbInnerFlowC = jobb.getExecutableNode("innerJobC");
+ ExecutableNode jobbInnerFlow = jobb.getExecutableNode("innerFlow");
+ assertNotNull(jobbInnerFlowA, jobbInnerFlowB, jobbInnerFlowC, jobbInnerFlow);
+
+ ExecutableNode jobcInnerFlowA = jobc.getExecutableNode("innerJobA");
+ ExecutableNode jobcInnerFlowB = jobc.getExecutableNode("innerJobB");
+ ExecutableNode jobcInnerFlowC = jobc.getExecutableNode("innerJobC");
+ ExecutableNode jobcInnerFlow = jobc.getExecutableNode("innerFlow");
+ assertNotNull(jobcInnerFlowA, jobcInnerFlowB, jobcInnerFlowC, jobcInnerFlow);
+
+ ExecutableNode jobdInnerFlowA = jobd.getExecutableNode("innerJobA");
+ ExecutableNode jobdInnerFlowB = jobd.getExecutableNode("innerJobB");
+ ExecutableNode jobdInnerFlowC = jobd.getExecutableNode("innerJobC");
+ ExecutableNode jobdInnerFlow = jobd.getExecutableNode("innerFlow");
+ assertNotNull(jobdInnerFlowA, jobdInnerFlowB, jobdInnerFlowC, jobdInnerFlow);
+
+ exFlow.setEndTime(1000);
+ exFlow.setStartTime(500);
+ exFlow.setStatus(Status.RUNNING);
+ exFlow.setUpdateTime(133);
+
+ // Change one job and see if it updates
+ long time = System.currentTimeMillis();
+ jobe.setEndTime(time);
+ jobe.setUpdateTime(time);
+ jobe.setStatus(Status.DISABLED);
+ jobe.setStartTime(time - 1);
+ // Should be one node that was changed
+ Map<String, Object> updateObject = exFlow.toUpdateObject(0);
+ Assert.assertEquals(1, ((List) (updateObject.get("nodes"))).size());
+ // Reapplying should give equal results.
+ copyFlow.applyUpdateObject(updateObject);
+ testEquals(exFlow, copyFlow);
+
+ // This update shouldn't provide any results
+ updateObject = exFlow.toUpdateObject(System.currentTimeMillis());
+ Assert.assertNull(updateObject.get("nodes"));
+
+ // Change inner flow
+ long currentTime = time + 1;
+ jobbInnerFlowA.setEndTime(currentTime);
+ jobbInnerFlowA.setUpdateTime(currentTime);
+ jobbInnerFlowA.setStatus(Status.DISABLED);
+ jobbInnerFlowA.setStartTime(currentTime - 100);
+ // We should get 2 updates if we do a toUpdateObject using 0 as the start
+ // time
+ updateObject = exFlow.toUpdateObject(0);
+ Assert.assertEquals(2, ((List) (updateObject.get("nodes"))).size());
+
+ // This should provide 1 update. That we can apply
+ updateObject = exFlow.toUpdateObject(jobe.getUpdateTime());
+ Assert.assertNotNull(updateObject.get("nodes"));
+ Assert.assertEquals(1, ((List) (updateObject.get("nodes"))).size());
+ copyFlow.applyUpdateObject(updateObject);
+ testEquals(exFlow, copyFlow);
+
+ // This shouldn't give any results anymore
+ updateObject = exFlow.toUpdateObject(jobbInnerFlowA.getUpdateTime());
+ Assert.assertNull(updateObject.get("nodes"));
+ }
+
+ private void assertNotNull(ExecutableNode... nodes) {
+ for (ExecutableNode node : nodes) {
+ Assert.assertNotNull(node);
+ }
+ }
+
+ public static void testEquals(ExecutableNode a, ExecutableNode b) {
+ if (a instanceof ExecutableFlow) {
+ if (b instanceof ExecutableFlow) {
+ ExecutableFlow exA = (ExecutableFlow) a;
+ ExecutableFlow exB = (ExecutableFlow) b;
+
+ Assert.assertEquals(exA.getScheduleId(), exB.getScheduleId());
+ Assert.assertEquals(exA.getProjectId(), exB.getProjectId());
+ Assert.assertEquals(exA.getVersion(), exB.getVersion());
+ Assert.assertEquals(exA.getSubmitTime(), exB.getSubmitTime());
+ Assert.assertEquals(exA.getSubmitUser(), exB.getSubmitUser());
+ Assert.assertEquals(exA.getExecutionPath(), exB.getExecutionPath());
+
+ testEquals(exA.getExecutionOptions(), exB.getExecutionOptions());
+ } else {
+ Assert.fail("A is ExecutableFlow, but B is not");
+ }
+ }
+
+ if (a instanceof ExecutableFlowBase) {
+ if (b instanceof ExecutableFlowBase) {
+ ExecutableFlowBase exA = (ExecutableFlowBase) a;
+ ExecutableFlowBase exB = (ExecutableFlowBase) b;
+
+ Assert.assertEquals(exA.getFlowId(), exB.getFlowId());
+ Assert.assertEquals(exA.getExecutableNodes().size(), exB
+ .getExecutableNodes().size());
+
+ for (ExecutableNode nodeA : exA.getExecutableNodes()) {
+ ExecutableNode nodeB = exB.getExecutableNode(nodeA.getId());
+ Assert.assertNotNull(nodeB);
+ Assert.assertEquals(a, nodeA.getParentFlow());
+ Assert.assertEquals(b, nodeB.getParentFlow());
+
+ testEquals(nodeA, nodeB);
+ }
+ } else {
+ Assert.fail("A is ExecutableFlowBase, but B is not");
+ }
+ }
+
+ Assert.assertEquals(a.getId(), b.getId());
+ Assert.assertEquals(a.getStatus(), b.getStatus());
+ Assert.assertEquals(a.getStartTime(), b.getStartTime());
+ Assert.assertEquals(a.getEndTime(), b.getEndTime());
+ Assert.assertEquals(a.getUpdateTime(), b.getUpdateTime());
+ Assert.assertEquals(a.getAttempt(), b.getAttempt());
+
+ Assert.assertEquals(a.getJobSource(), b.getJobSource());
+ Assert.assertEquals(a.getPropsSource(), b.getPropsSource());
+ Assert.assertEquals(a.getInNodes(), a.getInNodes());
+ Assert.assertEquals(a.getOutNodes(), a.getOutNodes());
+ }
+
+ public static void testEquals(ExecutionOptions optionsA,
+ ExecutionOptions optionsB) {
+ Assert.assertEquals(optionsA.getConcurrentOption(),
+ optionsB.getConcurrentOption());
+ Assert.assertEquals(optionsA.getNotifyOnFirstFailure(),
+ optionsB.getNotifyOnFirstFailure());
+ Assert.assertEquals(optionsA.getNotifyOnLastFailure(),
+ optionsB.getNotifyOnLastFailure());
+ Assert.assertEquals(optionsA.getFailureAction(),
+ optionsB.getFailureAction());
+ Assert.assertEquals(optionsA.getPipelineExecutionId(),
+ optionsB.getPipelineExecutionId());
+ Assert.assertEquals(optionsA.getPipelineLevel(),
+ optionsB.getPipelineLevel());
+ Assert.assertEquals(optionsA.isFailureEmailsOverridden(),
+ optionsB.isFailureEmailsOverridden());
+ Assert.assertEquals(optionsA.isSuccessEmailsOverridden(),
+ optionsB.isSuccessEmailsOverridden());
+
+ testDisabledEquals(optionsA.getDisabledJobs(), optionsB.getDisabledJobs());
+ testEquals(optionsA.getSuccessEmails(), optionsB.getSuccessEmails());
+ testEquals(optionsA.getFailureEmails(), optionsB.getFailureEmails());
+ testEquals(optionsA.getFlowParameters(), optionsB.getFlowParameters());
+ }
+
+ public static void testEquals(Set<String> a, Set<String> b) {
+ if (a == b) {
+ return;
+ }
+
+ if (a == null || b == null) {
+ Assert.fail();
+ }
+
+ Assert.assertEquals(a.size(), b.size());
+
+ Iterator<String> iterA = a.iterator();
+
+ while (iterA.hasNext()) {
+ String aStr = iterA.next();
+ Assert.assertTrue(b.contains(aStr));
+ }
+ }
+
+ public static void testEquals(List<String> a, List<String> b) {
+ if (a == b) {
+ return;
+ }
+
+ if (a == null || b == null) {
+ Assert.fail();
+ }
+
+ Assert.assertEquals(a.size(), b.size());
+
+ Iterator<String> iterA = a.iterator();
+ Iterator<String> iterB = b.iterator();
+
+ while (iterA.hasNext()) {
+ String aStr = iterA.next();
+ String bStr = iterB.next();
+ Assert.assertEquals(aStr, bStr);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static void testDisabledEquals(List<Object> a, List<Object> b) {
+ if (a == b) {
+ return;
+ }
+
+ if (a == null || b == null) {
+ Assert.fail();
+ }
+
+ Assert.assertEquals(a.size(), b.size());
+
+ Iterator<Object> iterA = a.iterator();
+ Iterator<Object> iterB = b.iterator();
+
+ while (iterA.hasNext()) {
+ Object aStr = iterA.next();
+ Object bStr = iterB.next();
+
+ if (aStr instanceof Map && bStr instanceof Map) {
+ Map<String, Object> aMap = (Map<String, Object>) aStr;
+ Map<String, Object> bMap = (Map<String, Object>) bStr;
+
+ Assert.assertEquals((String) aMap.get("id"), (String) bMap.get("id"));
+ testDisabledEquals((List<Object>) aMap.get("children"),
+ (List<Object>) bMap.get("children"));
+ } else {
+ Assert.assertEquals(aStr, bStr);
+ }
+ }
+ }
+
+ public static void testEquals(Map<String, String> a, Map<String, String> b) {
+ if (a == b) {
+ return;
+ }
+
+ if (a == null || b == null) {
+ Assert.fail();
+ }
+
+ Assert.assertEquals(a.size(), b.size());
+
+ for (String key : a.keySet()) {
+ Assert.assertEquals(a.get(key), b.get(key));
+ }
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/InteractiveTestJob.java b/azkaban-common/src/test/java/azkaban/executor/InteractiveTestJob.java
new file mode 100644
index 0000000..082adac
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/InteractiveTestJob.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2014 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.executor;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.log4j.Logger;
+
+import azkaban.flow.CommonJobProperties;
+import azkaban.jobExecutor.AbstractProcessJob;
+import azkaban.utils.Props;
+
+public class InteractiveTestJob extends AbstractProcessJob {
+ private static ConcurrentHashMap<String, InteractiveTestJob> testJobs =
+ new ConcurrentHashMap<String, InteractiveTestJob>();
+ private Props generatedProperties = new Props();
+ private boolean isWaiting = true;
+ private boolean succeed = true;
+
+ public static InteractiveTestJob getTestJob(String name) {
+ return testJobs.get(name);
+ }
+
+ public static void clearTestJobs() {
+ testJobs.clear();
+ }
+
+ public InteractiveTestJob(String jobId, Props sysProps, Props jobProps,
+ Logger log) {
+ super(jobId, sysProps, jobProps, log);
+ }
+
+ @Override
+ public void run() throws Exception {
+ String nestedFlowPath =
+ this.getJobProps().get(CommonJobProperties.NESTED_FLOW_PATH);
+ String groupName = this.getJobProps().getString("group", null);
+ String id = nestedFlowPath == null ? this.getId() : nestedFlowPath;
+ if (groupName != null) {
+ id = groupName + ":" + id;
+ }
+ testJobs.put(id, this);
+
+ while (isWaiting) {
+ synchronized (this) {
+ try {
+ wait(30000);
+ } catch (InterruptedException e) {
+ }
+
+ if (!isWaiting) {
+ if (!succeed) {
+ throw new RuntimeException("Forced failure of " + getId());
+ } else {
+ info("Job " + getId() + " succeeded.");
+ }
+ }
+ }
+ }
+ }
+
+ public void failJob() {
+ synchronized (this) {
+ succeed = false;
+ isWaiting = false;
+ this.notify();
+ }
+ }
+
+ public void succeedJob() {
+ synchronized (this) {
+ succeed = true;
+ isWaiting = false;
+ this.notify();
+ }
+ }
+
+ public void succeedJob(Props generatedProperties) {
+ synchronized (this) {
+ this.generatedProperties = generatedProperties;
+ succeed = true;
+ isWaiting = false;
+ this.notify();
+ }
+ }
+
+ @Override
+ public Props getJobGeneratedProperties() {
+ return generatedProperties;
+ }
+
+ @Override
+ public void cancel() throws InterruptedException {
+ info("Killing job");
+ failJob();
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/JavaJob.java b/azkaban-common/src/test/java/azkaban/executor/JavaJob.java
new file mode 100644
index 0000000..edba3db
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/JavaJob.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2014 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.
+ */
+
+package azkaban.executor;
+
+import java.io.File;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.apache.log4j.Logger;
+
+import azkaban.jobExecutor.JavaProcessJob;
+import azkaban.utils.Props;
+
+public class JavaJob extends JavaProcessJob {
+
+ public static final String RUN_METHOD_PARAM = "method.run";
+ public static final String CANCEL_METHOD_PARAM = "method.cancel";
+ public static final String PROGRESS_METHOD_PARAM = "method.progress";
+
+ public static final String JOB_CLASS = "job.class";
+ public static final String DEFAULT_CANCEL_METHOD = "cancel";
+ public static final String DEFAULT_RUN_METHOD = "run";
+ public static final String DEFAULT_PROGRESS_METHOD = "getProgress";
+
+ private String _runMethod;
+ private String _cancelMethod;
+ private String _progressMethod;
+
+ private Object _javaObject = null;
+ private String props;
+
+ public JavaJob(String jobid, Props sysProps, Props jobProps, Logger log) {
+ super(jobid, sysProps, new Props(sysProps, jobProps), log);
+ }
+
+ @Override
+ protected List<String> getClassPaths() {
+ List<String> classPath = super.getClassPaths();
+
+ classPath.add(getSourcePathFromClass(JavaJobRunnerMain.class));
+ classPath.add(getSourcePathFromClass(Props.class));
+
+ String loggerPath = getSourcePathFromClass(org.apache.log4j.Logger.class);
+ if (!classPath.contains(loggerPath)) {
+ classPath.add(loggerPath);
+ }
+
+ // Add hadoop home to classpath
+ String hadoopHome = System.getenv("HADOOP_HOME");
+ if (hadoopHome == null) {
+ info("HADOOP_HOME not set, using default hadoop config.");
+ } else {
+ info("Using hadoop config found in " + hadoopHome);
+ classPath.add(new File(hadoopHome, "conf").getPath());
+ }
+ return classPath;
+ }
+
+ private static String getSourcePathFromClass(Class<?> containedClass) {
+ File file =
+ new File(containedClass.getProtectionDomain().getCodeSource()
+ .getLocation().getPath());
+
+ if (!file.isDirectory() && file.getName().endsWith(".class")) {
+ String name = containedClass.getName();
+ StringTokenizer tokenizer = new StringTokenizer(name, ".");
+ while (tokenizer.hasMoreTokens()) {
+ tokenizer.nextElement();
+ file = file.getParentFile();
+ }
+ return file.getPath();
+ } else {
+ return containedClass.getProtectionDomain().getCodeSource().getLocation()
+ .getPath();
+ }
+ }
+
+ @Override
+ protected String getJavaClass() {
+ return JavaJobRunnerMain.class.getName();
+ }
+
+ @Override
+ public String toString() {
+ return "JavaJob{" + "_runMethod='" + _runMethod + '\''
+ + ", _cancelMethod='" + _cancelMethod + '\'' + ", _progressMethod='"
+ + _progressMethod + '\'' + ", _javaObject=" + _javaObject + ", props="
+ + props + '}';
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/JavaJobRunnerMain.java b/azkaban-common/src/test/java/azkaban/executor/JavaJobRunnerMain.java
new file mode 100644
index 0000000..67d0284
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/JavaJobRunnerMain.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2014 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.
+ */
+
+package azkaban.executor;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Layout;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+
+import azkaban.jobExecutor.ProcessJob;
+import azkaban.utils.Props;
+
+public class JavaJobRunnerMain {
+
+ public static final String JOB_CLASS = "job.class";
+ public static final String DEFAULT_RUN_METHOD = "run";
+ public static final String DEFAULT_CANCEL_METHOD = "cancel";
+
+ // This is the Job interface method to get the properties generated by the
+ // job.
+ public static final String GET_GENERATED_PROPERTIES_METHOD =
+ "getJobGeneratedProperties";
+
+ public static final String CANCEL_METHOD_PARAM = "method.cancel";
+ public static final String RUN_METHOD_PARAM = "method.run";
+ public static final String[] PROPS_CLASSES = new String[] {
+ "azkaban.utils.Props", "azkaban.common.utils.Props" };
+
+ private static final Layout DEFAULT_LAYOUT = new PatternLayout("%p %m\n");
+
+ public final Logger _logger;
+
+ public String _cancelMethod;
+ public String _jobName;
+ public Object _javaObject;
+ private boolean _isFinished = false;
+
+ public static void main(String[] args) throws Exception {
+ @SuppressWarnings("unused")
+ JavaJobRunnerMain wrapper = new JavaJobRunnerMain();
+ }
+
+ public JavaJobRunnerMain() throws Exception {
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ public void run() {
+ cancelJob();
+ }
+ });
+
+ try {
+ _jobName = System.getenv(ProcessJob.JOB_NAME_ENV);
+ String propsFile = System.getenv(ProcessJob.JOB_PROP_ENV);
+
+ _logger = Logger.getRootLogger();
+ _logger.removeAllAppenders();
+ ConsoleAppender appender = new ConsoleAppender(DEFAULT_LAYOUT);
+ appender.activateOptions();
+ _logger.addAppender(appender);
+
+ Properties prop = new Properties();
+ prop.load(new BufferedReader(new FileReader(propsFile)));
+
+ _logger.info("Running job " + _jobName);
+ String className = prop.getProperty(JOB_CLASS);
+ if (className == null) {
+ throw new Exception("Class name is not set.");
+ }
+ _logger.info("Class name " + className);
+
+ // Create the object using proxy
+
+ _javaObject = getObject(_jobName, className, prop, _logger);
+
+ if (_javaObject == null) {
+ _logger.info("Could not create java object to run job: " + className);
+ throw new Exception("Could not create running object");
+ }
+
+ _cancelMethod =
+ prop.getProperty(CANCEL_METHOD_PARAM, DEFAULT_CANCEL_METHOD);
+
+ final String runMethod =
+ prop.getProperty(RUN_METHOD_PARAM, DEFAULT_RUN_METHOD);
+ _logger.info("Invoking method " + runMethod);
+
+ _logger.info("Proxy check failed, not proxying run.");
+ runMethod(_javaObject, runMethod);
+
+ _isFinished = true;
+
+ // Get the generated properties and store them to disk, to be read
+ // by ProcessJob.
+ try {
+ final Method generatedPropertiesMethod =
+ _javaObject.getClass().getMethod(GET_GENERATED_PROPERTIES_METHOD,
+ new Class<?>[] {});
+ Object outputGendProps =
+ generatedPropertiesMethod.invoke(_javaObject, new Object[] {});
+ if (outputGendProps != null) {
+ final Method toPropertiesMethod =
+ outputGendProps.getClass().getMethod("toProperties",
+ new Class<?>[] {});
+ Properties properties =
+ (Properties) toPropertiesMethod.invoke(outputGendProps,
+ new Object[] {});
+
+ Props outputProps = new Props(null, properties);
+ outputGeneratedProperties(outputProps);
+ } else {
+ outputGeneratedProperties(new Props());
+ }
+
+ } catch (NoSuchMethodException e) {
+ _logger
+ .info(String
+ .format(
+ "Apparently there isn't a method[%s] on object[%s], using empty Props object instead.",
+ GET_GENERATED_PROPERTIES_METHOD, _javaObject));
+ outputGeneratedProperties(new Props());
+ }
+ } catch (Exception e) {
+ _isFinished = true;
+ throw e;
+ }
+ }
+
+ private void runMethod(Object obj, String runMethod)
+ throws IllegalAccessException, InvocationTargetException,
+ NoSuchMethodException {
+ obj.getClass().getMethod(runMethod, new Class<?>[] {}).invoke(obj);
+ }
+
+ private void outputGeneratedProperties(Props outputProperties) {
+
+ if (outputProperties == null) {
+ _logger.info(" no gend props");
+ return;
+ }
+ for (String key : outputProperties.getKeySet()) {
+ _logger
+ .info(" gend prop " + key + " value:" + outputProperties.get(key));
+ }
+
+ String outputFileStr = System.getenv(ProcessJob.JOB_OUTPUT_PROP_FILE);
+ if (outputFileStr == null) {
+ return;
+ }
+
+ _logger.info("Outputting generated properties to " + outputFileStr);
+
+ Map<String, String> properties = new LinkedHashMap<String, String>();
+ for (String key : outputProperties.getKeySet()) {
+ properties.put(key, outputProperties.get(key));
+ }
+
+ OutputStream writer = null;
+ try {
+ writer = new BufferedOutputStream(new FileOutputStream(outputFileStr));
+
+ // Manually serialize into JSON instead of adding org.json to
+ // external classpath. Reduces one dependency for something that's
+ // essentially easy.
+ writer.write("{\n".getBytes());
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ writer.write(String.format(" \"%s\":\"%s\",\n",
+ entry.getKey().replace("\"", "\\\\\""),
+ entry.getValue().replace("\"", "\\\\\"")).getBytes());
+ }
+ writer.write("}".getBytes());
+ } catch (Exception e) {
+ new RuntimeException("Unable to store output properties to: "
+ + outputFileStr);
+ } finally {
+ try {
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ public void cancelJob() {
+ if (_isFinished) {
+ return;
+ }
+ _logger.info("Attempting to call cancel on this job");
+ if (_javaObject != null) {
+ Method method = null;
+
+ try {
+ method = _javaObject.getClass().getMethod(_cancelMethod);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ }
+
+ if (method != null)
+ try {
+ method.invoke(_javaObject);
+ } catch (Exception e) {
+ if (_logger != null) {
+ _logger.error("Cancel method failed! ", e);
+ }
+ }
+ else {
+ throw new RuntimeException("Job " + _jobName
+ + " does not have cancel method " + _cancelMethod);
+ }
+ }
+ }
+
+ private static Object getObject(String jobName, String className,
+ Properties properties, Logger logger) throws Exception {
+
+ Class<?> runningClass =
+ JavaJobRunnerMain.class.getClassLoader().loadClass(className);
+
+ if (runningClass == null) {
+ throw new Exception("Class " + className
+ + " was not found. Cannot run job.");
+ }
+
+ Class<?> propsClass = null;
+ for (String propClassName : PROPS_CLASSES) {
+ try {
+ propsClass =
+ JavaJobRunnerMain.class.getClassLoader().loadClass(propClassName);
+ } catch (ClassNotFoundException e) {
+ }
+
+ if (propsClass != null
+ && getConstructor(runningClass, String.class, propsClass) != null) {
+ // is this the props class
+ break;
+ }
+ propsClass = null;
+ }
+
+ Object obj = null;
+ if (propsClass != null
+ && getConstructor(runningClass, String.class, propsClass) != null) {
+ // Create props class
+ Constructor<?> propsCon =
+ getConstructor(propsClass, propsClass, Properties[].class);
+ Object props =
+ propsCon.newInstance(null, new Properties[] { properties });
+
+ Constructor<?> con =
+ getConstructor(runningClass, String.class, propsClass);
+ logger.info("Constructor found " + con.toGenericString());
+ obj = con.newInstance(jobName, props);
+ } else if (getConstructor(runningClass, String.class, Properties.class) != null) {
+
+ Constructor<?> con =
+ getConstructor(runningClass, String.class, Properties.class);
+ logger.info("Constructor found " + con.toGenericString());
+ obj = con.newInstance(jobName, properties);
+ } else if (getConstructor(runningClass, String.class, Map.class) != null) {
+ Constructor<?> con =
+ getConstructor(runningClass, String.class, Map.class);
+ logger.info("Constructor found " + con.toGenericString());
+
+ HashMap<Object, Object> map = new HashMap<Object, Object>();
+ for (Map.Entry<Object, Object> entry : properties.entrySet()) {
+ map.put(entry.getKey(), entry.getValue());
+ }
+ obj = con.newInstance(jobName, map);
+ } else if (getConstructor(runningClass, String.class) != null) {
+ Constructor<?> con = getConstructor(runningClass, String.class);
+ logger.info("Constructor found " + con.toGenericString());
+ obj = con.newInstance(jobName);
+ } else if (getConstructor(runningClass) != null) {
+ Constructor<?> con = getConstructor(runningClass);
+ logger.info("Constructor found " + con.toGenericString());
+ obj = con.newInstance();
+ } else {
+ logger.error("Constructor not found. Listing available Constructors.");
+ for (Constructor<?> c : runningClass.getConstructors()) {
+ logger.info(c.toGenericString());
+ }
+ }
+ return obj;
+ }
+
+ private static Constructor<?> getConstructor(Class<?> c, Class<?>... args) {
+ try {
+ Constructor<?> cons = c.getConstructor(args);
+ return cons;
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/JdbcExecutorLoaderTest.java b/azkaban-common/src/test/java/azkaban/executor/JdbcExecutorLoaderTest.java
new file mode 100644
index 0000000..f453f0c
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/JdbcExecutorLoaderTest.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2014 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.executor;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.dbutils.DbUtils;
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.dbutils.ResultSetHandler;
+
+import org.joda.time.DateTime;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.database.DataSourceUtils;
+import azkaban.flow.Flow;
+import azkaban.project.Project;
+import azkaban.utils.FileIOUtils.LogData;
+import azkaban.utils.JSONUtils;
+import azkaban.utils.Pair;
+import azkaban.utils.Props;
+
+public class JdbcExecutorLoaderTest {
+ private static boolean testDBExists;
+ // @TODO remove this and turn into local host.
+ private static final String host = "cyu-ld.linkedin.biz";
+ private static final int port = 3306;
+ private static final String database = "azkaban2";
+ private static final String user = "azkaban";
+ private static final String password = "azkaban";
+ private static final int numConnections = 10;
+
+ private File flowDir = new File("unit/executions/exectest1");
+
+ @BeforeClass
+ public static void setupDB() {
+ DataSource dataSource =
+ DataSourceUtils.getMySQLDataSource(host, port, database, user,
+ password, numConnections);
+ testDBExists = true;
+
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ CountHandler countHandler = new CountHandler();
+ QueryRunner runner = new QueryRunner();
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM active_executing_flows",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM execution_flows",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM execution_jobs",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM execution_logs",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ DbUtils.closeQuietly(connection);
+
+ clearDB();
+ }
+
+ private static void clearDB() {
+ if (!testDBExists) {
+ return;
+ }
+
+ DataSource dataSource =
+ DataSourceUtils.getMySQLDataSource(host, port, database, user,
+ password, numConnections);
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ QueryRunner runner = new QueryRunner();
+ try {
+ runner.update(connection, "DELETE FROM active_executing_flows");
+
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM execution_flows");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM execution_jobs");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM execution_logs");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ DbUtils.closeQuietly(connection);
+ }
+
+ @Test
+ public void testUploadExecutionFlows() throws Exception {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ExecutorLoader loader = createLoader();
+ ExecutableFlow flow = createExecutableFlow("exec1");
+
+ loader.uploadExecutableFlow(flow);
+
+ ExecutableFlow fetchFlow =
+ loader.fetchExecutableFlow(flow.getExecutionId());
+
+ // Shouldn't be the same object.
+ Assert.assertTrue(flow != fetchFlow);
+ Assert.assertEquals(flow.getExecutionId(), fetchFlow.getExecutionId());
+ Assert.assertEquals(flow.getEndTime(), fetchFlow.getEndTime());
+ Assert.assertEquals(flow.getStartTime(), fetchFlow.getStartTime());
+ Assert.assertEquals(flow.getSubmitTime(), fetchFlow.getSubmitTime());
+ Assert.assertEquals(flow.getFlowId(), fetchFlow.getFlowId());
+ Assert.assertEquals(flow.getProjectId(), fetchFlow.getProjectId());
+ Assert.assertEquals(flow.getVersion(), fetchFlow.getVersion());
+ Assert.assertEquals(flow.getExecutionOptions().getFailureAction(),
+ fetchFlow.getExecutionOptions().getFailureAction());
+ Assert.assertEquals(new HashSet<String>(flow.getEndNodes()),
+ new HashSet<String>(fetchFlow.getEndNodes()));
+ }
+
+ @Test
+ public void testUpdateExecutionFlows() throws Exception {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ExecutorLoader loader = createLoader();
+ ExecutableFlow flow = createExecutableFlow("exec1");
+
+ loader.uploadExecutableFlow(flow);
+
+ ExecutableFlow fetchFlow2 =
+ loader.fetchExecutableFlow(flow.getExecutionId());
+
+ fetchFlow2.setEndTime(System.currentTimeMillis());
+ fetchFlow2.setStatus(Status.SUCCEEDED);
+ loader.updateExecutableFlow(fetchFlow2);
+ ExecutableFlow fetchFlow =
+ loader.fetchExecutableFlow(flow.getExecutionId());
+
+ // Shouldn't be the same object.
+ Assert.assertTrue(flow != fetchFlow);
+ Assert.assertEquals(flow.getExecutionId(), fetchFlow.getExecutionId());
+ Assert.assertEquals(fetchFlow2.getEndTime(), fetchFlow.getEndTime());
+ Assert.assertEquals(fetchFlow2.getStatus(), fetchFlow.getStatus());
+ Assert.assertEquals(flow.getStartTime(), fetchFlow.getStartTime());
+ Assert.assertEquals(flow.getSubmitTime(), fetchFlow.getSubmitTime());
+ Assert.assertEquals(flow.getFlowId(), fetchFlow.getFlowId());
+ Assert.assertEquals(flow.getProjectId(), fetchFlow.getProjectId());
+ Assert.assertEquals(flow.getVersion(), fetchFlow.getVersion());
+ Assert.assertEquals(flow.getExecutionOptions().getFailureAction(),
+ fetchFlow.getExecutionOptions().getFailureAction());
+ Assert.assertEquals(new HashSet<String>(flow.getEndNodes()),
+ new HashSet<String>(fetchFlow.getEndNodes()));
+ }
+
+ @Test
+ public void testUploadExecutableNode() throws Exception {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ExecutorLoader loader = createLoader();
+ ExecutableFlow flow = createExecutableFlow(10, "exec1");
+ flow.setExecutionId(10);
+
+ File jobFile = new File(flowDir, "job10.job");
+ Props props = new Props(null, jobFile);
+ props.put("test", "test2");
+ ExecutableNode oldNode = flow.getExecutableNode("job10");
+ oldNode.setStartTime(System.currentTimeMillis());
+ loader.uploadExecutableNode(oldNode, props);
+
+ ExecutableJobInfo info = loader.fetchJobInfo(10, "job10", 0);
+ Assert.assertEquals(flow.getExecutionId(), info.getExecId());
+ Assert.assertEquals(flow.getProjectId(), info.getProjectId());
+ Assert.assertEquals(flow.getVersion(), info.getVersion());
+ Assert.assertEquals(flow.getFlowId(), info.getFlowId());
+ Assert.assertEquals(oldNode.getId(), info.getJobId());
+ Assert.assertEquals(oldNode.getStatus(), info.getStatus());
+ Assert.assertEquals(oldNode.getStartTime(), info.getStartTime());
+ Assert.assertEquals("endTime = " + oldNode.getEndTime()
+ + " info endTime = " + info.getEndTime(), oldNode.getEndTime(),
+ info.getEndTime());
+
+ // Fetch props
+ Props outputProps = new Props();
+ outputProps.put("hello", "output");
+ oldNode.setOutputProps(outputProps);
+ oldNode.setEndTime(System.currentTimeMillis());
+ loader.updateExecutableNode(oldNode);
+
+ Props fInputProps = loader.fetchExecutionJobInputProps(10, "job10");
+ Props fOutputProps = loader.fetchExecutionJobOutputProps(10, "job10");
+ Pair<Props, Props> inOutProps = loader.fetchExecutionJobProps(10, "job10");
+
+ Assert.assertEquals(fInputProps.get("test"), "test2");
+ Assert.assertEquals(fOutputProps.get("hello"), "output");
+ Assert.assertEquals(inOutProps.getFirst().get("test"), "test2");
+ Assert.assertEquals(inOutProps.getSecond().get("hello"), "output");
+
+ }
+
+ @Test
+ public void testActiveReference() throws Exception {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ExecutorLoader loader = createLoader();
+ ExecutableFlow flow1 = createExecutableFlow("exec1");
+ loader.uploadExecutableFlow(flow1);
+ ExecutionReference ref1 =
+ new ExecutionReference(flow1.getExecutionId(), "test", 1);
+ loader.addActiveExecutableReference(ref1);
+
+ ExecutableFlow flow2 = createExecutableFlow("exec1");
+ loader.uploadExecutableFlow(flow2);
+ ExecutionReference ref2 =
+ new ExecutionReference(flow2.getExecutionId(), "test", 1);
+ loader.addActiveExecutableReference(ref2);
+
+ ExecutableFlow flow3 = createExecutableFlow("exec1");
+ loader.uploadExecutableFlow(flow3);
+
+ Map<Integer, Pair<ExecutionReference, ExecutableFlow>> activeFlows1 =
+ loader.fetchActiveFlows();
+ ExecutableFlow flow1Result =
+ activeFlows1.get(flow1.getExecutionId()).getSecond();
+ Assert.assertNotNull(flow1Result);
+ Assert.assertTrue(flow1 != flow1Result);
+ Assert.assertEquals(flow1.getExecutionId(), flow1Result.getExecutionId());
+ Assert.assertEquals(flow1.getEndTime(), flow1Result.getEndTime());
+ Assert.assertEquals(flow1.getStartTime(), flow1Result.getStartTime());
+ Assert.assertEquals(flow1.getSubmitTime(), flow1Result.getSubmitTime());
+ Assert.assertEquals(flow1.getFlowId(), flow1Result.getFlowId());
+ Assert.assertEquals(flow1.getProjectId(), flow1Result.getProjectId());
+ Assert.assertEquals(flow1.getVersion(), flow1Result.getVersion());
+ Assert.assertEquals(flow1.getExecutionOptions().getFailureAction(),
+ flow1Result.getExecutionOptions().getFailureAction());
+
+ ExecutableFlow flow1Result2 =
+ activeFlows1.get(flow2.getExecutionId()).getSecond();
+ Assert.assertNotNull(flow1Result2);
+ Assert.assertTrue(flow2 != flow1Result2);
+ Assert.assertEquals(flow2.getExecutionId(), flow1Result2.getExecutionId());
+ Assert.assertEquals(flow2.getEndTime(), flow1Result2.getEndTime());
+ Assert.assertEquals(flow2.getStartTime(), flow1Result2.getStartTime());
+ Assert.assertEquals(flow2.getSubmitTime(), flow1Result2.getSubmitTime());
+ Assert.assertEquals(flow2.getFlowId(), flow1Result2.getFlowId());
+ Assert.assertEquals(flow2.getProjectId(), flow1Result2.getProjectId());
+ Assert.assertEquals(flow2.getVersion(), flow1Result2.getVersion());
+ Assert.assertEquals(flow2.getExecutionOptions().getFailureAction(),
+ flow1Result2.getExecutionOptions().getFailureAction());
+
+ loader.removeActiveExecutableReference(flow2.getExecutionId());
+ Map<Integer, Pair<ExecutionReference, ExecutableFlow>> activeFlows2 =
+ loader.fetchActiveFlows();
+
+ Assert.assertTrue(activeFlows2.containsKey(flow1.getExecutionId()));
+ Assert.assertFalse(activeFlows2.containsKey(flow3.getExecutionId()));
+ Assert.assertFalse(activeFlows2.containsKey(flow2.getExecutionId()));
+ }
+
+ @Ignore @Test
+ public void testSmallUploadLog() throws ExecutorManagerException {
+ File logDir = new File("unit/executions/logtest");
+ File[] smalllog =
+ { new File(logDir, "log1.log"), new File(logDir, "log2.log"),
+ new File(logDir, "log3.log") };
+
+ ExecutorLoader loader = createLoader();
+ loader.uploadLogFile(1, "smallFiles", 0, smalllog);
+
+ LogData data = loader.fetchLogs(1, "smallFiles", 0, 0, 50000);
+ Assert.assertNotNull(data);
+ Assert.assertEquals("Logs length is " + data.getLength(), data.getLength(),
+ 53);
+
+ System.out.println(data.toString());
+
+ LogData data2 = loader.fetchLogs(1, "smallFiles", 0, 10, 20);
+ System.out.println(data2.toString());
+ Assert.assertNotNull(data2);
+ Assert.assertEquals("Logs length is " + data2.getLength(),
+ data2.getLength(), 20);
+
+ }
+
+ @Ignore @Test
+ public void testLargeUploadLog() throws ExecutorManagerException {
+ File logDir = new File("unit/executions/logtest");
+
+ // Multiple of 255 for Henry the Eigth
+ File[] largelog =
+ { new File(logDir, "largeLog1.log"), new File(logDir, "largeLog2.log"),
+ new File(logDir, "largeLog3.log") };
+
+ ExecutorLoader loader = createLoader();
+ loader.uploadLogFile(1, "largeFiles", 0, largelog);
+
+ LogData logsResult = loader.fetchLogs(1, "largeFiles", 0, 0, 64000);
+ Assert.assertNotNull(logsResult);
+ Assert.assertEquals("Logs length is " + logsResult.getLength(),
+ logsResult.getLength(), 64000);
+
+ LogData logsResult2 = loader.fetchLogs(1, "largeFiles", 0, 1000, 64000);
+ Assert.assertNotNull(logsResult2);
+ Assert.assertEquals("Logs length is " + logsResult2.getLength(),
+ logsResult2.getLength(), 64000);
+
+ LogData logsResult3 = loader.fetchLogs(1, "largeFiles", 0, 330000, 400000);
+ Assert.assertNotNull(logsResult3);
+ Assert.assertEquals("Logs length is " + logsResult3.getLength(),
+ logsResult3.getLength(), 5493);
+
+ LogData logsResult4 = loader.fetchLogs(1, "largeFiles", 0, 340000, 400000);
+ Assert.assertNull(logsResult4);
+
+ LogData logsResult5 = loader.fetchLogs(1, "largeFiles", 0, 153600, 204800);
+ Assert.assertNotNull(logsResult5);
+ Assert.assertEquals("Logs length is " + logsResult5.getLength(),
+ logsResult5.getLength(), 181893);
+
+ LogData logsResult6 = loader.fetchLogs(1, "largeFiles", 0, 150000, 250000);
+ Assert.assertNotNull(logsResult6);
+ Assert.assertEquals("Logs length is " + logsResult6.getLength(),
+ logsResult6.getLength(), 185493);
+ }
+
+ @SuppressWarnings("static-access")
+ @Ignore @Test
+ public void testRemoveExecutionLogsByTime() throws ExecutorManagerException,
+ IOException, InterruptedException {
+
+ ExecutorLoader loader = createLoader();
+
+ File logDir = new File("unit/executions/logtest");
+
+ // Multiple of 255 for Henry the Eigth
+ File[] largelog =
+ { new File(logDir, "largeLog1.log"), new File(logDir, "largeLog2.log"),
+ new File(logDir, "largeLog3.log") };
+
+ DateTime time1 = DateTime.now();
+ loader.uploadLogFile(1, "oldlog", 0, largelog);
+ // sleep for 5 seconds
+ Thread.currentThread().sleep(5000);
+ loader.uploadLogFile(2, "newlog", 0, largelog);
+
+ DateTime time2 = time1.plusMillis(2500);
+
+ int count = loader.removeExecutionLogsByTime(time2.getMillis());
+ System.out.print("Removed " + count + " records");
+ LogData logs = loader.fetchLogs(1, "oldlog", 0, 0, 22222);
+ Assert.assertTrue(logs == null);
+ logs = loader.fetchLogs(2, "newlog", 0, 0, 22222);
+ Assert.assertFalse(logs == null);
+ }
+
+ private ExecutableFlow createExecutableFlow(int executionId, String flowName)
+ throws IOException {
+ File jsonFlowFile = new File(flowDir, flowName + ".flow");
+ @SuppressWarnings("unchecked")
+ HashMap<String, Object> flowObj =
+ (HashMap<String, Object>) JSONUtils.parseJSONFromFile(jsonFlowFile);
+
+ Flow flow = Flow.flowFromObject(flowObj);
+ Project project = new Project(1, "flow");
+ HashMap<String, Flow> flowMap = new HashMap<String, Flow>();
+ flowMap.put(flow.getId(), flow);
+ project.setFlows(flowMap);
+ ExecutableFlow execFlow = new ExecutableFlow(project, flow);
+ execFlow.setExecutionId(executionId);
+
+ return execFlow;
+ }
+
+ private ExecutableFlow createExecutableFlow(String flowName)
+ throws IOException {
+ File jsonFlowFile = new File(flowDir, flowName + ".flow");
+ @SuppressWarnings("unchecked")
+ HashMap<String, Object> flowObj =
+ (HashMap<String, Object>) JSONUtils.parseJSONFromFile(jsonFlowFile);
+
+ Flow flow = Flow.flowFromObject(flowObj);
+ Project project = new Project(1, "flow");
+ HashMap<String, Flow> flowMap = new HashMap<String, Flow>();
+ flowMap.put(flow.getId(), flow);
+ project.setFlows(flowMap);
+ ExecutableFlow execFlow = new ExecutableFlow(project, flow);
+
+ return execFlow;
+ }
+
+ private ExecutorLoader createLoader() {
+ Props props = new Props();
+ props.put("database.type", "mysql");
+
+ props.put("mysql.host", host);
+ props.put("mysql.port", port);
+ props.put("mysql.user", user);
+ props.put("mysql.database", database);
+ props.put("mysql.password", password);
+ props.put("mysql.numconnections", numConnections);
+
+ return new JdbcExecutorLoader(props);
+ }
+
+ private boolean isTestSetup() {
+ if (!testDBExists) {
+ System.err.println("Skipping DB test because Db not setup.");
+ return false;
+ }
+
+ System.out.println("Running DB test because Db setup.");
+ return true;
+ }
+
+ public static class CountHandler implements ResultSetHandler<Integer> {
+ @Override
+ public Integer handle(ResultSet rs) throws SQLException {
+ int val = 0;
+ while (rs.next()) {
+ val++;
+ }
+
+ return val;
+ }
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/MockExecutorLoader.java b/azkaban-common/src/test/java/azkaban/executor/MockExecutorLoader.java
new file mode 100644
index 0000000..b9ad178
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/MockExecutorLoader.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2014 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.executor;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import azkaban.utils.FileIOUtils.LogData;
+import azkaban.utils.Pair;
+import azkaban.utils.Props;
+
+public class MockExecutorLoader implements ExecutorLoader {
+
+ HashMap<Integer, ExecutableFlow> flows =
+ new HashMap<Integer, ExecutableFlow>();
+ HashMap<String, ExecutableNode> nodes = new HashMap<String, ExecutableNode>();
+ HashMap<Integer, ExecutionReference> refs =
+ new HashMap<Integer, ExecutionReference>();
+ int flowUpdateCount = 0;
+ HashMap<String, Integer> jobUpdateCount = new HashMap<String, Integer>();
+ Map<Integer, Pair<ExecutionReference, ExecutableFlow>> activeFlows =
+ new HashMap<Integer, Pair<ExecutionReference, ExecutableFlow>>();
+
+ @Override
+ public void uploadExecutableFlow(ExecutableFlow flow)
+ throws ExecutorManagerException {
+ flows.put(flow.getExecutionId(), flow);
+ flowUpdateCount++;
+ }
+
+ @Override
+ public ExecutableFlow fetchExecutableFlow(int execId)
+ throws ExecutorManagerException {
+ ExecutableFlow flow = flows.get(execId);
+ return ExecutableFlow.createExecutableFlowFromObject(flow.toObject());
+ }
+
+ @Override
+ public Map<Integer, Pair<ExecutionReference, ExecutableFlow>> fetchActiveFlows()
+ throws ExecutorManagerException {
+ return activeFlows;
+ }
+
+ @Override
+ public List<ExecutableFlow> fetchFlowHistory(int projectId, String flowId,
+ int skip, int num) throws ExecutorManagerException {
+ return null;
+ }
+
+ @Override
+ public void addActiveExecutableReference(ExecutionReference ref)
+ throws ExecutorManagerException {
+ refs.put(ref.getExecId(), ref);
+ }
+
+ @Override
+ public void removeActiveExecutableReference(int execId)
+ throws ExecutorManagerException {
+ refs.remove(execId);
+ }
+
+ public boolean hasActiveExecutableReference(int execId) {
+ return refs.containsKey(execId);
+ }
+
+ @Override
+ public void uploadLogFile(int execId, String name, int attempt, File... files)
+ throws ExecutorManagerException {
+
+ }
+
+ @Override
+ public void updateExecutableFlow(ExecutableFlow flow)
+ throws ExecutorManagerException {
+ ExecutableFlow toUpdate = flows.get(flow.getExecutionId());
+
+ toUpdate.applyUpdateObject((Map<String, Object>) flow.toUpdateObject(0));
+ flowUpdateCount++;
+ }
+
+ @Override
+ public void uploadExecutableNode(ExecutableNode node, Props inputParams)
+ throws ExecutorManagerException {
+ ExecutableNode exNode = new ExecutableNode();
+ exNode.fillExecutableFromMapObject(node.toObject());
+
+ nodes.put(node.getId(), exNode);
+ jobUpdateCount.put(node.getId(), 1);
+ }
+
+ @Override
+ public void updateExecutableNode(ExecutableNode node)
+ throws ExecutorManagerException {
+ ExecutableNode foundNode = nodes.get(node.getId());
+ foundNode.setEndTime(node.getEndTime());
+ foundNode.setStartTime(node.getStartTime());
+ foundNode.setStatus(node.getStatus());
+ foundNode.setUpdateTime(node.getUpdateTime());
+
+ Integer value = jobUpdateCount.get(node.getId());
+ if (value == null) {
+ throw new ExecutorManagerException("The node has not been uploaded");
+ } else {
+ jobUpdateCount.put(node.getId(), ++value);
+ }
+
+ flowUpdateCount++;
+ }
+
+ @Override
+ public int fetchNumExecutableFlows(int projectId, String flowId)
+ throws ExecutorManagerException {
+ return 0;
+ }
+
+ @Override
+ public int fetchNumExecutableFlows() throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ public int getFlowUpdateCount() {
+ return flowUpdateCount;
+ }
+
+ public Integer getNodeUpdateCount(String jobId) {
+ return jobUpdateCount.get(jobId);
+ }
+
+ @Override
+ public ExecutableJobInfo fetchJobInfo(int execId, String jobId, int attempt)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean updateExecutableReference(int execId, long updateTime)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return true;
+ }
+
+ @Override
+ public LogData fetchLogs(int execId, String name, int attempt, int startByte,
+ int endByte) throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<ExecutableFlow> fetchFlowHistory(int skip, int num)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<ExecutableFlow> fetchFlowHistory(String projectContains,
+ String flowContains, String userNameContains, int status, long startData,
+ long endData, int skip, int num) throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<ExecutableJobInfo> fetchJobHistory(int projectId, String jobId,
+ int skip, int size) throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int fetchNumExecutableNodes(int projectId, String jobId)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public Props fetchExecutionJobInputProps(int execId, String jobId)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Props fetchExecutionJobOutputProps(int execId, String jobId)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Pair<Props, Props> fetchExecutionJobProps(int execId, String jobId)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<ExecutableJobInfo> fetchJobInfoAttempts(int execId, String jobId)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int removeExecutionLogsByTime(long millis)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public List<ExecutableFlow> fetchFlowHistory(int projectId, String flowId,
+ int skip, int num, Status status) throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<Object> fetchAttachments(int execId, String name, int attempt)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void uploadAttachmentFile(ExecutableNode node, File file)
+ throws ExecutorManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/executor/SleepJavaJob.java b/azkaban-common/src/test/java/azkaban/executor/SleepJavaJob.java
new file mode 100644
index 0000000..6b447dc
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/executor/SleepJavaJob.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014 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.executor;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.util.Map;
+import java.util.Properties;
+
+public class SleepJavaJob {
+ private boolean fail;
+ private String seconds;
+ private int attempts;
+ private int currentAttempt;
+
+ public SleepJavaJob(String id, Properties props) {
+ setup(props);
+ }
+
+ public SleepJavaJob(String id, Map<String, String> parameters) {
+ Properties properties = new Properties();
+ properties.putAll(parameters);
+
+ setup(properties);
+ }
+
+ private void setup(Properties props) {
+ String failStr = (String) props.get("fail");
+
+ if (failStr == null || failStr.equals("false")) {
+ fail = false;
+ } else {
+ fail = true;
+ }
+
+ currentAttempt =
+ props.containsKey("azkaban.job.attempt") ? Integer
+ .parseInt((String) props.get("azkaban.job.attempt")) : 0;
+ String attemptString = (String) props.get("passRetry");
+ if (attemptString == null) {
+ attempts = -1;
+ } else {
+ attempts = Integer.valueOf(attemptString);
+ }
+ seconds = (String) props.get("seconds");
+
+ if (fail) {
+ System.out.println("Planning to fail after " + seconds
+ + " seconds. Attempts left " + currentAttempt + " of " + attempts);
+ } else {
+ System.out.println("Planning to succeed after " + seconds + " seconds.");
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ String propsFile = System.getenv("JOB_PROP_FILE");
+ Properties prop = new Properties();
+ prop.load(new BufferedReader(new FileReader(propsFile)));
+
+ String jobName = System.getenv("JOB_NAME");
+ SleepJavaJob job = new SleepJavaJob(jobName, prop);
+
+ job.run();
+ }
+
+ public void run() throws Exception {
+ if (seconds == null) {
+ throw new RuntimeException("Seconds not set");
+ }
+
+ int sec = Integer.parseInt(seconds);
+ System.out.println("Sec " + sec);
+ synchronized (this) {
+ try {
+ this.wait(sec * 1000);
+ } catch (InterruptedException e) {
+ System.out.println("Interrupted " + fail);
+ }
+ }
+
+ if (fail) {
+ if (attempts <= 0 || currentAttempt <= attempts) {
+ throw new Exception("I failed because I had to.");
+ }
+ }
+ }
+
+ public void cancel() throws Exception {
+ System.out.println("Cancelled called on Sleep job");
+ fail = true;
+ synchronized (this) {
+ this.notifyAll();
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobExecutor/AllJobExecutorTests.java b/azkaban-common/src/test/java/azkaban/jobExecutor/AllJobExecutorTests.java
new file mode 100644
index 0000000..c41bbfe
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobExecutor/AllJobExecutorTests.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 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.jobExecutor;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({ JavaProcessJobTest.class, ProcessJobTest.class,
+ PythonJobTest.class })
+public class AllJobExecutorTests {
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobExecutor/JavaProcessJobTest.java b/azkaban-common/src/test/java/azkaban/jobExecutor/JavaProcessJobTest.java
new file mode 100644
index 0000000..11c799c
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobExecutor/JavaProcessJobTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2014 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.jobExecutor;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Properties;
+
+import org.apache.log4j.Logger;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.utils.Props;
+
+public class JavaProcessJobTest {
+
+ private JavaProcessJob job = null;
+ // private JobDescriptor descriptor = null;
+ private Props props = null;
+ private Logger log = Logger.getLogger(JavaProcessJob.class);
+ private static String classPaths;
+
+ private static final String inputContent =
+ "Quick Change in Strategy for a Bookseller \n"
+ + " By JULIE BOSMAN \n"
+ + "Published: August 11, 2010 \n"
+ + " \n"
+ + "Twelve years later, it may be Joe Fox's turn to worry. Readers have gone from skipping small \n"
+ + "bookstores to wondering if they need bookstores at all. More people are ordering books online \n"
+ + "or plucking them from the best-seller bin at Wal-Mart";
+
+ private static final String errorInputContent =
+ inputContent
+ + "\n stop_here "
+ + "But the threat that has the industry and some readers the most rattled is the growth of e-books. \n"
+ + " In the first five months of 2009, e-books made up 2.9 percent of trade book sales. In the same period \n"
+ + "in 2010, sales of e-books, which generally cost less than hardcover books, grew to 8.5 percent, according \n"
+ + "to the Association of American Publishers, spurred by sales of the Amazon Kindle and the new Apple iPad. \n"
+ + "For Barnes & Noble, long the largest and most powerful bookstore chain in the country, the new competition \n"
+ + "has led to declining profits and store traffic.";
+
+ private static String inputFile;
+ private static String errorInputFile;
+ private static String outputFile;
+
+ @BeforeClass
+ public static void init() {
+ // get the classpath
+ Properties prop = System.getProperties();
+ classPaths =
+ String.format("'%s'", prop.getProperty("java.class.path", null));
+
+ long time = (new Date()).getTime();
+ inputFile = "/tmp/azkaban_input_" + time;
+ errorInputFile = "/tmp/azkaban_input_error_" + time;
+ outputFile = "/tmp/azkaban_output_" + time;
+ // dump input files
+ try {
+ Utils.dumpFile(inputFile, inputContent);
+ Utils.dumpFile(errorInputFile, errorInputContent);
+ } catch (IOException e) {
+ e.printStackTrace(System.err);
+ Assert.fail("error in creating input file:" + e.getLocalizedMessage());
+ }
+
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ // remove the input file and error input file
+ Utils.removeFile(inputFile);
+ Utils.removeFile(errorInputFile);
+ // Utils.removeFile(outputFile);
+ }
+
+ @Before
+ public void setUp() {
+
+ /* initialize job */
+ // descriptor = EasyMock.createMock(JobDescriptor.class);
+
+ props = new Props();
+ props.put(AbstractProcessJob.WORKING_DIR, ".");
+ props.put("type", "java");
+ props.put("fullPath", ".");
+
+ // EasyMock.expect(descriptor.getId()).andReturn("java").times(1);
+ // EasyMock.expect(descriptor.getProps()).andReturn(props).times(1);
+ // EasyMock.expect(descriptor.getFullPath()).andReturn(".").times(1);
+ //
+ // EasyMock.replay(descriptor);
+
+ job = new JavaProcessJob("testJavaProcess", props, props, log);
+
+ // EasyMock.verify(descriptor);
+ }
+
+ @Ignore @Test
+ public void testJavaJob() throws Exception {
+ /* initialize the Props */
+ props.put(JavaProcessJob.JAVA_CLASS,
+ "azkaban.test.jobExecutor.WordCountLocal");
+ props.put(ProcessJob.WORKING_DIR, ".");
+ props.put("input", inputFile);
+ props.put("output", outputFile);
+ props.put("classpath", classPaths);
+ job.run();
+ }
+
+ @Ignore @Test
+ public void testJavaJobHashmap() throws Exception {
+ /* initialize the Props */
+ props.put(JavaProcessJob.JAVA_CLASS, "azkaban.test.executor.SleepJavaJob");
+ props.put("seconds", 1);
+ props.put(ProcessJob.WORKING_DIR, ".");
+ props.put("input", inputFile);
+ props.put("output", outputFile);
+ props.put("classpath", classPaths);
+ job.run();
+ }
+
+ @Test
+ public void testFailedJavaJob() throws Exception {
+ props.put(JavaProcessJob.JAVA_CLASS,
+ "azkaban.test.jobExecutor.WordCountLocal");
+ props.put(ProcessJob.WORKING_DIR, ".");
+ props.put("input", errorInputFile);
+ props.put("output", outputFile);
+ props.put("classpath", classPaths);
+
+ try {
+ job.run();
+ } catch (RuntimeException e) {
+ Assert.assertTrue(true);
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobExecutor/ProcessJobTest.java b/azkaban-common/src/test/java/azkaban/jobExecutor/ProcessJobTest.java
new file mode 100644
index 0000000..26b86d5
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobExecutor/ProcessJobTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2014 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.jobExecutor;
+
+import org.apache.log4j.Logger;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import azkaban.utils.Props;
+
+public class ProcessJobTest {
+ private ProcessJob job = null;
+ // private JobDescriptor descriptor = null;
+ private Props props = null;
+ private Logger log = Logger.getLogger(ProcessJob.class);
+
+ @Before
+ public void setUp() {
+
+ /* initialize job */
+ // props = EasyMock.createMock(Props.class);
+
+ props = new Props();
+ props.put(AbstractProcessJob.WORKING_DIR, ".");
+ props.put("type", "command");
+ props.put("fullPath", ".");
+
+ // EasyMock.expect(props.getString("type")).andReturn("command").times(1);
+ // EasyMock.expect(props.getProps()).andReturn(props).times(1);
+ // EasyMock.expect(props.getString("fullPath")).andReturn(".").times(1);
+ //
+ // EasyMock.replay(props);
+
+ job = new ProcessJob("TestProcess", props, props, log);
+
+ }
+
+ @Test
+ public void testOneUnixCommand() throws Exception {
+ /* initialize the Props */
+ props.put(ProcessJob.COMMAND, "ls -al");
+ props.put(ProcessJob.WORKING_DIR, ".");
+
+ job.run();
+
+ }
+
+ @Test
+ public void testFailedUnixCommand() throws Exception {
+ /* initialize the Props */
+ props.put(ProcessJob.COMMAND, "xls -al");
+ props.put(ProcessJob.WORKING_DIR, ".");
+
+ try {
+ job.run();
+ } catch (RuntimeException e) {
+ Assert.assertTrue(true);
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testMultipleUnixCommands() throws Exception {
+ /* initialize the Props */
+ props.put(ProcessJob.WORKING_DIR, ".");
+ props.put(ProcessJob.COMMAND, "pwd");
+ props.put("command.1", "date");
+ props.put("command.2", "whoami");
+
+ job.run();
+ }
+
+ @Test
+ public void testPartitionCommand() throws Exception {
+ String test1 = "a b c";
+
+ Assert.assertArrayEquals(new String[] { "a", "b", "c" },
+ ProcessJob.partitionCommandLine(test1));
+
+ String test2 = "a 'b c'";
+ Assert.assertArrayEquals(new String[] { "a", "b c" },
+ ProcessJob.partitionCommandLine(test2));
+
+ String test3 = "a e='b c'";
+ Assert.assertArrayEquals(new String[] { "a", "e=b c" },
+ ProcessJob.partitionCommandLine(test3));
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobExecutor/PythonJobTest.java b/azkaban-common/src/test/java/azkaban/jobExecutor/PythonJobTest.java
new file mode 100644
index 0000000..ee767de
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobExecutor/PythonJobTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2014 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.jobExecutor;
+
+import java.io.IOException;
+import java.util.Date;
+
+import org.apache.log4j.Logger;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import azkaban.utils.Props;
+
+public class PythonJobTest {
+ private PythonJob job = null;
+ // private JobDescriptor descriptor = null;
+ private Props props = null;
+ private Logger log = Logger.getLogger(PythonJob.class);
+
+ private static final String scriptContent =
+ "#!/usr/bin/python \n" +
+ "import re, string, sys \n" +
+ "# if no arguments were given, print a helpful message \n" +
+ "l=len(sys.argv) \n" +
+ "if l < 1: \n"+
+ "\tprint 'Usage: celsium --t temp' \n" +
+ "\tsys.exit(1) \n" +
+ "\n" +
+ "# Loop over the arguments \n" +
+ "i=1 \n" +
+ "while i < l-1 : \n" +
+ "\tname = sys.argv[i] \n" +
+ "\tvalue = sys.argv[i+1] \n" +
+ "\tif name == \"--t\": \n" +
+ "\t\ttry: \n" +
+ "\t\t\tfahrenheit = float(string.atoi(value)) \n" +
+ "\t\texcept string.atoi_error: \n" +
+ "\t\t\tprint repr(value), \" not a numeric value\" \n" +
+ "\t\telse: \n" +
+ "\t\t\tcelsius=(fahrenheit-32)*5.0/9.0 \n" +
+ "\t\t\tprint '%i F = %iC' % (int(fahrenheit), int(celsius+.5)) \n" +
+ "\t\t\tsys.exit(0) \n" +
+ "\t\ti=i+2\n";
+
+ private static String scriptFile;
+
+ @BeforeClass
+ public static void init() {
+
+ long time = (new Date()).getTime();
+ scriptFile = "/tmp/azkaban_python" + time + ".py";
+ // dump script file
+ try {
+ Utils.dumpFile(scriptFile, scriptContent);
+ } catch (IOException e) {
+ e.printStackTrace(System.err);
+ Assert.fail("error in creating script file:" + e.getLocalizedMessage());
+ }
+
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ // remove the input file and error input file
+ Utils.removeFile(scriptFile);
+ }
+
+ @Test
+ public void testPythonJob() {
+
+ /* initialize job */
+ // descriptor = EasyMock.createMock(JobDescriptor.class);
+
+ props = new Props();
+ props.put(AbstractProcessJob.WORKING_DIR, ".");
+ props.put("type", "python");
+ props.put("script", scriptFile);
+ props.put("t", "90");
+ props.put("type", "script");
+ props.put("fullPath", ".");
+
+ // EasyMock.expect(descriptor.getId()).andReturn("script").times(1);
+ // EasyMock.expect(descriptor.getProps()).andReturn(props).times(3);
+ // EasyMock.expect(descriptor.getFullPath()).andReturn(".").times(1);
+ // EasyMock.replay(descriptor);
+ job = new PythonJob("TestProcess", props, props, log);
+ // EasyMock.verify(descriptor);
+ try {
+ job.run();
+ } catch (Exception e) {
+ e.printStackTrace(System.err);
+ Assert.fail("Python job failed:" + e.getLocalizedMessage());
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobExecutor/Utils.java b/azkaban-common/src/test/java/azkaban/jobExecutor/Utils.java
new file mode 100644
index 0000000..ad5f353
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobExecutor/Utils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2014 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.jobExecutor;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public class Utils {
+ private Utils() {
+ }
+
+ public static void dumpFile(String filename, String filecontent)
+ throws IOException {
+ PrintWriter writer = new PrintWriter(new FileWriter(filename));
+ writer.print(filecontent);
+ writer.close();
+ }
+
+ public static void removeFile(String filename) {
+ File file = new File(filename);
+ file.delete();
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobExecutor/WordCountLocal.java b/azkaban-common/src/test/java/azkaban/jobExecutor/WordCountLocal.java
new file mode 100644
index 0000000..7a9545a
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobExecutor/WordCountLocal.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2014 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.jobExecutor;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import org.apache.log4j.Logger;
+
+import azkaban.utils.Props;
+
+public class WordCountLocal extends AbstractJob {
+
+ private String _input = null;
+ private String _output = null;
+ private Map<String, Integer> _dic = new HashMap<String, Integer>();
+
+ public static void main(String[] args) throws Exception {
+ String propsFile = System.getenv(ProcessJob.JOB_PROP_ENV);
+ System.out.println("propsFile: " + propsFile);
+ Props prop = new Props(null, propsFile);
+ WordCountLocal instance = new WordCountLocal("", prop);
+ instance.run();
+ }
+
+ public WordCountLocal(String id, Props prop) {
+ super(id, Logger.getLogger(WordCountLocal.class));
+ _input = prop.getString("input");
+ _output = prop.getString("output");
+ }
+
+ public void run() throws Exception {
+
+ if (_input == null)
+ throw new Exception("input file is null");
+ if (_output == null)
+ throw new Exception("output file is null");
+ BufferedReader in =
+ new BufferedReader(new InputStreamReader(new FileInputStream(_input)));
+
+ String line = null;
+ while ((line = in.readLine()) != null) {
+ StringTokenizer tokenizer = new StringTokenizer(line);
+ while (tokenizer.hasMoreTokens()) {
+ String word = tokenizer.nextToken();
+
+ if (word.toString().equals("end_here")) { // expect an out-of-bound
+ // exception
+ String[] errArray = new String[1];
+ System.out.println("string in possition 2 is " + errArray[1]);
+ }
+
+ if (_dic.containsKey(word)) {
+ Integer num = _dic.get(word);
+ _dic.put(word, num + 1);
+ } else {
+ _dic.put(word, 1);
+ }
+ }
+ }
+ in.close();
+
+ PrintWriter out = new PrintWriter(new FileOutputStream(_output));
+ for (Map.Entry<String, Integer> entry : _dic.entrySet()) {
+ out.println(entry.getKey() + "\t" + entry.getValue());
+ }
+ out.close();
+ }
+
+ @Override
+ public Props getJobGeneratedProperties() {
+ return new Props();
+ }
+
+ @Override
+ public boolean isCanceled() {
+ return false;
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobtype/FakeJavaJob.java b/azkaban-common/src/test/java/azkaban/jobtype/FakeJavaJob.java
new file mode 100644
index 0000000..cc41f53
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobtype/FakeJavaJob.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 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.jobtype;
+
+import org.apache.log4j.Logger;
+
+import azkaban.jobExecutor.JavaProcessJob;
+import azkaban.utils.Props;
+
+public class FakeJavaJob extends JavaProcessJob {
+ public FakeJavaJob(String jobid, Props sysProps, Props jobProps, Logger log) {
+ super(jobid, sysProps, jobProps, log);
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobtype/FakeJavaJob2.java b/azkaban-common/src/test/java/azkaban/jobtype/FakeJavaJob2.java
new file mode 100644
index 0000000..ccbd2ed
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobtype/FakeJavaJob2.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 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.jobtype;
+
+import org.apache.log4j.Logger;
+
+import azkaban.jobExecutor.JavaProcessJob;
+import azkaban.utils.Props;
+
+public class FakeJavaJob2 extends JavaProcessJob {
+ public FakeJavaJob2(String jobid, Props sysProps, Props jobProps, Logger log) {
+ super(jobid, sysProps, jobProps, log);
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/jobtype/JobTypeManagerTest.java b/azkaban-common/src/test/java/azkaban/jobtype/JobTypeManagerTest.java
new file mode 100644
index 0000000..7a42262
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/jobtype/JobTypeManagerTest.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2014 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.jobtype;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import azkaban.jobExecutor.Job;
+import azkaban.utils.Props;
+
+/**
+ * Test the flow run, especially with embedded flows. Files are in
+ * unit/plugins/jobtypes
+ *
+ */
+public class JobTypeManagerTest {
+ public static String TEST_PLUGIN_DIR = "jobtypes_test";
+ private Logger logger = Logger.getLogger(JobTypeManagerTest.class);
+ private JobTypeManager manager;
+
+ public JobTypeManagerTest() {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ File jobTypeDir = new File(TEST_PLUGIN_DIR);
+ jobTypeDir.mkdirs();
+
+ FileUtils.copyDirectory(new File("unit/plugins/jobtypes"), jobTypeDir);
+ manager =
+ new JobTypeManager(TEST_PLUGIN_DIR, null, this.getClass()
+ .getClassLoader());
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ FileUtils.deleteDirectory(new File(TEST_PLUGIN_DIR));
+ }
+
+ /**
+ * Tests that the common and common private properties are loaded correctly
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testCommonPluginProps() throws Exception {
+ JobTypePluginSet pluginSet = manager.getJobTypePluginSet();
+
+ Props props = pluginSet.getCommonPluginJobProps();
+ System.out.println(props.toString());
+ assertEquals("commonprop1", props.getString("commonprop1"));
+ assertEquals("commonprop2", props.getString("commonprop2"));
+ assertEquals("commonprop3", props.getString("commonprop3"));
+
+ Props priv = pluginSet.getCommonPluginLoadProps();
+ assertEquals("commonprivate1", priv.getString("commonprivate1"));
+ assertEquals("commonprivate2", priv.getString("commonprivate2"));
+ assertEquals("commonprivate3", priv.getString("commonprivate3"));
+ }
+
+ /**
+ * Tests that the proper classes were loaded and that the common and the load
+ * properties are properly loaded.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testLoadedClasses() throws Exception {
+ JobTypePluginSet pluginSet = manager.getJobTypePluginSet();
+
+ Props props = pluginSet.getCommonPluginJobProps();
+ System.out.println(props.toString());
+ assertEquals("commonprop1", props.getString("commonprop1"));
+ assertEquals("commonprop2", props.getString("commonprop2"));
+ assertEquals("commonprop3", props.getString("commonprop3"));
+ assertNull(props.get("commonprivate1"));
+
+ Props priv = pluginSet.getCommonPluginLoadProps();
+ assertEquals("commonprivate1", priv.getString("commonprivate1"));
+ assertEquals("commonprivate2", priv.getString("commonprivate2"));
+ assertEquals("commonprivate3", priv.getString("commonprivate3"));
+
+ // Testing the anothertestjobtype
+ Class<? extends Job> aPluginClass =
+ pluginSet.getPluginClass("anothertestjob");
+ assertEquals("azkaban.test.jobtype.FakeJavaJob", aPluginClass.getName());
+ Props ajobProps = pluginSet.getPluginJobProps("anothertestjob");
+ Props aloadProps = pluginSet.getPluginLoaderProps("anothertestjob");
+
+ // Loader props
+ assertEquals("lib/*", aloadProps.get("jobtype.classpath"));
+ assertEquals("azkaban.test.jobtype.FakeJavaJob",
+ aloadProps.get("jobtype.class"));
+ assertEquals("commonprivate1", aloadProps.get("commonprivate1"));
+ assertEquals("commonprivate2", aloadProps.get("commonprivate2"));
+ assertEquals("commonprivate3", aloadProps.get("commonprivate3"));
+ // Job props
+ assertEquals("commonprop1", ajobProps.get("commonprop1"));
+ assertEquals("commonprop2", ajobProps.get("commonprop2"));
+ assertEquals("commonprop3", ajobProps.get("commonprop3"));
+ assertNull(ajobProps.get("commonprivate1"));
+
+ Class<? extends Job> tPluginClass = pluginSet.getPluginClass("testjob");
+ assertEquals("azkaban.test.jobtype.FakeJavaJob2", tPluginClass.getName());
+ Props tjobProps = pluginSet.getPluginJobProps("testjob");
+ Props tloadProps = pluginSet.getPluginLoaderProps("testjob");
+
+ // Loader props
+ assertNull(tloadProps.get("jobtype.classpath"));
+ assertEquals("azkaban.test.jobtype.FakeJavaJob2",
+ tloadProps.get("jobtype.class"));
+ assertEquals("commonprivate1", tloadProps.get("commonprivate1"));
+ assertEquals("commonprivate2", tloadProps.get("commonprivate2"));
+ assertEquals("private3", tloadProps.get("commonprivate3"));
+ assertEquals("0", tloadProps.get("testprivate"));
+ // Job props
+ assertEquals("commonprop1", tjobProps.get("commonprop1"));
+ assertEquals("commonprop2", tjobProps.get("commonprop2"));
+ assertEquals("1", tjobProps.get("pluginprops1"));
+ assertEquals("2", tjobProps.get("pluginprops2"));
+ assertEquals("3", tjobProps.get("pluginprops3"));
+ assertEquals("pluginprops", tjobProps.get("commonprop3"));
+ // Testing that the private properties aren't shared with the public ones
+ assertNull(tjobProps.get("commonprivate1"));
+ assertNull(tjobProps.get("testprivate"));
+ }
+
+ /**
+ * Test building classes
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testBuildClass() throws Exception {
+ Props jobProps = new Props();
+ jobProps.put("type", "anothertestjob");
+ jobProps.put("test", "test1");
+ jobProps.put("pluginprops3", "4");
+ Job job = manager.buildJobExecutor("anothertestjob", jobProps, logger);
+
+ assertTrue(job instanceof FakeJavaJob);
+ FakeJavaJob fjj = (FakeJavaJob) job;
+
+ Props props = fjj.getJobProps();
+ assertEquals("test1", props.get("test"));
+ assertNull(props.get("pluginprops1"));
+ assertEquals("4", props.get("pluginprops3"));
+ assertEquals("commonprop1", props.get("commonprop1"));
+ assertEquals("commonprop2", props.get("commonprop2"));
+ assertEquals("commonprop3", props.get("commonprop3"));
+ assertNull(props.get("commonprivate1"));
+ }
+
+ /**
+ * Test building classes 2
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testBuildClass2() throws Exception {
+ Props jobProps = new Props();
+ jobProps.put("type", "testjob");
+ jobProps.put("test", "test1");
+ jobProps.put("pluginprops3", "4");
+ Job job = manager.buildJobExecutor("testjob", jobProps, logger);
+
+ assertTrue(job instanceof FakeJavaJob2);
+ FakeJavaJob2 fjj = (FakeJavaJob2) job;
+
+ Props props = fjj.getJobProps();
+ assertEquals("test1", props.get("test"));
+ assertEquals("1", props.get("pluginprops1"));
+ assertEquals("2", props.get("pluginprops2"));
+ assertEquals("4", props.get("pluginprops3")); // Overridden value
+ assertEquals("commonprop1", props.get("commonprop1"));
+ assertEquals("commonprop2", props.get("commonprop2"));
+ assertEquals("pluginprops", props.get("commonprop3"));
+ assertNull(props.get("commonprivate1"));
+ }
+
+ /**
+ * Test out reloading properties
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testResetPlugins() throws Exception {
+ // Add a plugins file to the anothertestjob folder
+ File anothertestfolder = new File(TEST_PLUGIN_DIR + "/anothertestjob");
+ Props pluginProps = new Props();
+ pluginProps.put("test1", "1");
+ pluginProps.put("test2", "2");
+ pluginProps.put("pluginprops3", "4");
+ pluginProps
+ .storeFlattened(new File(anothertestfolder, "plugin.properties"));
+
+ // clone the testjob folder
+ File testFolder = new File(TEST_PLUGIN_DIR + "/testjob");
+ FileUtils.copyDirectory(testFolder, new File(TEST_PLUGIN_DIR
+ + "/newtestjob"));
+
+ // change the common properties
+ Props commonPlugin =
+ new Props(null, TEST_PLUGIN_DIR + "/common.properties");
+ commonPlugin.put("commonprop1", "1");
+ commonPlugin.put("newcommonprop1", "2");
+ commonPlugin.removeLocal("commonprop2");
+ commonPlugin
+ .storeFlattened(new File(TEST_PLUGIN_DIR + "/common.properties"));
+
+ // change the common properties
+ Props commonPrivate =
+ new Props(null, TEST_PLUGIN_DIR + "/commonprivate.properties");
+ commonPrivate.put("commonprivate1", "1");
+ commonPrivate.put("newcommonprivate1", "2");
+ commonPrivate.removeLocal("commonprivate2");
+ commonPrivate.storeFlattened(new File(TEST_PLUGIN_DIR
+ + "/commonprivate.properties"));
+
+ // change testjob private property
+ Props loadProps =
+ new Props(null, TEST_PLUGIN_DIR + "/testjob/private.properties");
+ loadProps.put("privatetest", "test");
+
+ /*
+ * Reload the plugins here!!
+ */
+ manager.loadPlugins();
+
+ // Checkout common props
+ JobTypePluginSet pluginSet = manager.getJobTypePluginSet();
+ Props commonProps = pluginSet.getCommonPluginJobProps();
+ assertEquals("1", commonProps.get("commonprop1"));
+ assertEquals("commonprop3", commonProps.get("commonprop3"));
+ assertEquals("2", commonProps.get("newcommonprop1"));
+ assertNull(commonProps.get("commonprop2"));
+
+ // Checkout common private
+ Props commonPrivateProps = pluginSet.getCommonPluginLoadProps();
+ assertEquals("1", commonPrivateProps.get("commonprivate1"));
+ assertEquals("commonprivate3", commonPrivateProps.get("commonprivate3"));
+ assertEquals("2", commonPrivateProps.get("newcommonprivate1"));
+ assertNull(commonPrivateProps.get("commonprivate2"));
+
+ // Verify anothertestjob changes
+ Class<? extends Job> atjClass = pluginSet.getPluginClass("anothertestjob");
+ assertEquals("azkaban.test.jobtype.FakeJavaJob", atjClass.getName());
+ Props ajobProps = pluginSet.getPluginJobProps("anothertestjob");
+ assertEquals("1", ajobProps.get("test1"));
+ assertEquals("2", ajobProps.get("test2"));
+ assertEquals("4", ajobProps.get("pluginprops3"));
+ assertEquals("commonprop3", ajobProps.get("commonprop3"));
+
+ Props aloadProps = pluginSet.getPluginLoaderProps("anothertestjob");
+ assertEquals("1", aloadProps.get("commonprivate1"));
+ assertNull(aloadProps.get("commonprivate2"));
+ assertEquals("commonprivate3", aloadProps.get("commonprivate3"));
+
+ // Verify testjob changes
+ Class<? extends Job> tjClass = pluginSet.getPluginClass("testjob");
+ assertEquals("azkaban.test.jobtype.FakeJavaJob2", tjClass.getName());
+ Props tjobProps = pluginSet.getPluginJobProps("testjob");
+ assertEquals("1", tjobProps.get("commonprop1"));
+ assertEquals("2", tjobProps.get("newcommonprop1"));
+ assertEquals("1", tjobProps.get("pluginprops1"));
+ assertEquals("2", tjobProps.get("pluginprops2"));
+ assertEquals("3", tjobProps.get("pluginprops3"));
+ assertEquals("pluginprops", tjobProps.get("commonprop3"));
+ assertNull(tjobProps.get("commonprop2"));
+
+ Props tloadProps = pluginSet.getPluginLoaderProps("testjob");
+ assertNull(tloadProps.get("jobtype.classpath"));
+ assertEquals("azkaban.test.jobtype.FakeJavaJob2",
+ tloadProps.get("jobtype.class"));
+ assertEquals("1", tloadProps.get("commonprivate1"));
+ assertNull(tloadProps.get("commonprivate2"));
+ assertEquals("private3", tloadProps.get("commonprivate3"));
+
+ // Verify newtestjob
+ Class<? extends Job> ntPluginClass = pluginSet.getPluginClass("newtestjob");
+ assertEquals("azkaban.test.jobtype.FakeJavaJob2", ntPluginClass.getName());
+ Props ntjobProps = pluginSet.getPluginJobProps("newtestjob");
+ Props ntloadProps = pluginSet.getPluginLoaderProps("newtestjob");
+
+ // Loader props
+ assertNull(ntloadProps.get("jobtype.classpath"));
+ assertEquals("azkaban.test.jobtype.FakeJavaJob2",
+ ntloadProps.get("jobtype.class"));
+ assertEquals("1", ntloadProps.get("commonprivate1"));
+ assertNull(ntloadProps.get("commonprivate2"));
+ assertEquals("private3", ntloadProps.get("commonprivate3"));
+ assertEquals("0", ntloadProps.get("testprivate"));
+ // Job props
+ assertEquals("1", ntjobProps.get("commonprop1"));
+ assertNull(ntjobProps.get("commonprop2"));
+ assertEquals("1", ntjobProps.get("pluginprops1"));
+ assertEquals("2", ntjobProps.get("pluginprops2"));
+ assertEquals("3", ntjobProps.get("pluginprops3"));
+ assertEquals("pluginprops", ntjobProps.get("commonprop3"));
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/project/JdbcProjectLoaderTest.java b/azkaban-common/src/test/java/azkaban/project/JdbcProjectLoaderTest.java
new file mode 100644
index 0000000..a7ef5f3
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/project/JdbcProjectLoaderTest.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2014 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.project;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.dbutils.DbUtils;
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.dbutils.ResultSetHandler;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.database.DataSourceUtils;
+import azkaban.flow.Edge;
+import azkaban.flow.Flow;
+import azkaban.flow.Node;
+import azkaban.project.ProjectLogEvent.EventType;
+import azkaban.user.Permission;
+import azkaban.user.User;
+import azkaban.utils.Pair;
+import azkaban.utils.Props;
+import azkaban.utils.PropsUtils;
+
+public class JdbcProjectLoaderTest {
+ private static boolean testDBExists;
+ private static final String host = "localhost";
+ private static final int port = 3306;
+ private static final String database = "test";
+ private static final String user = "azkaban";
+ private static final String password = "azkaban";
+ private static final int numConnections = 10;
+
+ @BeforeClass
+ public static void setupDB() {
+ DataSource dataSource =
+ DataSourceUtils.getMySQLDataSource(host, port, database, user,
+ password, numConnections);
+ testDBExists = true;
+
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ CountHandler countHandler = new CountHandler();
+ QueryRunner runner = new QueryRunner();
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM projects", countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM project_events",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM project_permissions",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM project_files",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM project_flows",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM project_properties",
+ countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ DbUtils.closeQuietly(connection);
+
+ clearDB();
+ }
+
+ private static void clearDB() {
+ if (!testDBExists) {
+ return;
+ }
+
+ DataSource dataSource =
+ DataSourceUtils.getMySQLDataSource(host, port, database, user,
+ password, numConnections);
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ QueryRunner runner = new QueryRunner();
+ try {
+ runner.update(connection, "DELETE FROM projects");
+
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM project_events");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM project_permissions");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM project_files");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM project_flows");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ runner.update(connection, "DELETE FROM project_properties");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ DbUtils.closeQuietly(connection);
+ }
+
+ @Test
+ public void testCreateProject() throws ProjectManagerException {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ProjectLoader loader = createLoader();
+ String projectName = "mytestProject";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+ Assert.assertTrue("Project Id set", project.getId() > -1);
+ Assert.assertEquals("Project name", projectName, project.getName());
+ Assert.assertEquals("Project description", projectDescription,
+ project.getDescription());
+
+ System.out.println("Test true");
+ Project project2 = loader.fetchProjectById(project.getId());
+ assertProjectMemberEquals(project, project2);
+ }
+
+ @Test
+ public void testRemoveProject() throws ProjectManagerException {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ProjectLoader loader = createLoader();
+ String projectName = "testRemoveProject";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+ Assert.assertTrue("Project Id set", project.getId() > -1);
+ Assert.assertEquals("Project name", projectName, project.getName());
+ Assert.assertEquals("Project description", projectDescription,
+ project.getDescription());
+
+ Project project2 = loader.fetchProjectById(project.getId());
+ assertProjectMemberEquals(project, project2);
+ loader.removeProject(project, user.getUserId());
+
+ Project project3 = loader.fetchProjectById(project.getId());
+ Assert.assertFalse(project3.isActive());
+
+ List<Project> projList = loader.fetchAllActiveProjects();
+ for (Project proj : projList) {
+ Assert.assertTrue(proj.getId() != project.getId());
+ }
+ }
+
+ @Test
+ public void testAddRemovePermissions() throws ProjectManagerException {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ProjectLoader loader = createLoader();
+ String projectName = "mytestProject1";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+ Assert.assertTrue("Project Id set", project.getId() > -1);
+ Assert.assertEquals("Project name", projectName, project.getName());
+ Assert.assertEquals("Project description", projectDescription,
+ project.getDescription());
+
+ Permission perm = new Permission(0x2);
+ loader.updatePermission(project, user.getUserId(), new Permission(0x2),
+ false);
+ loader.updatePermission(project, "group1", new Permission(0x2), true);
+ Assert.assertEquals(perm, project.getUserPermission(user.getUserId()));
+
+ Permission permOverride = new Permission(0x6);
+ loader.updatePermission(project, user.getUserId(), permOverride, false);
+ Assert.assertEquals(permOverride,
+ project.getUserPermission(user.getUserId()));
+
+ Project project2 = loader.fetchProjectById(project.getId());
+ assertProjectMemberEquals(project, project2);
+ Assert.assertEquals(permOverride,
+ project2.getUserPermission(user.getUserId()));
+ }
+
+ @Test
+ public void testProjectEventLogs() throws ProjectManagerException {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ProjectLoader loader = createLoader();
+ String projectName = "testProjectEventLogs";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ String message = "My message";
+ EventType type = EventType.USER_PERMISSION;
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+ loader.postEvent(project, type, user.getUserId(), message);
+
+ List<ProjectLogEvent> events = loader.getProjectEvents(project, 10, 0);
+ Assert.assertTrue(events.size() == 1);
+
+ ProjectLogEvent event = events.get(0);
+ Assert.assertEquals(event.getProjectId(), project.getId());
+ Assert.assertEquals(event.getUser(), user.getUserId());
+ Assert.assertEquals(event.getMessage(), message);
+ Assert.assertEquals(event.getType(), type);
+ }
+
+ @Ignore @Test
+ public void testFlowUpload() throws ProjectManagerException {
+ ProjectLoader loader = createLoader();
+ ((JdbcProjectLoader) loader)
+ .setDefaultEncodingType(JdbcProjectLoader.EncodingType.GZIP);
+ String projectName = "mytestFlowUpload1";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+
+ Flow flow = new Flow("MyNewFlow");
+
+ flow.addNode(new Node("A"));
+ flow.addNode(new Node("B"));
+ flow.addNode(new Node("C"));
+ flow.addNode(new Node("D"));
+
+ flow.addEdge(new Edge("A", "B"));
+ flow.addEdge(new Edge("A", "C"));
+ flow.addEdge(new Edge("B", "D"));
+ flow.addEdge(new Edge("C", "D"));
+
+ flow.initialize();
+
+ loader.uploadFlow(project, 4, flow);
+ project.setVersion(4);
+ Flow newFlow = loader.fetchFlow(project, flow.getId());
+ Assert.assertTrue(newFlow != null);
+ Assert.assertEquals(flow.getId(), newFlow.getId());
+ Assert.assertEquals(flow.getEdges().size(), newFlow.getEdges().size());
+ Assert.assertEquals(flow.getNodes().size(), newFlow.getNodes().size());
+ }
+
+ @Ignore @Test
+ public void testFlowUploadPlain() throws ProjectManagerException {
+ ProjectLoader loader = createLoader();
+ ((JdbcProjectLoader) loader)
+ .setDefaultEncodingType(JdbcProjectLoader.EncodingType.PLAIN);
+ String projectName = "mytestFlowUpload2";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+
+ Flow flow = new Flow("MyNewFlow2");
+
+ flow.addNode(new Node("A1"));
+ flow.addNode(new Node("B1"));
+ flow.addNode(new Node("C1"));
+ flow.addNode(new Node("D1"));
+
+ flow.addEdge(new Edge("A1", "B1"));
+ flow.addEdge(new Edge("A1", "C1"));
+ flow.addEdge(new Edge("B1", "D1"));
+ flow.addEdge(new Edge("C1", "D1"));
+
+ flow.initialize();
+
+ loader.uploadFlow(project, 4, flow);
+ project.setVersion(4);
+ Flow newFlow = loader.fetchFlow(project, flow.getId());
+ Assert.assertTrue(newFlow != null);
+ Assert.assertEquals(flow.getId(), newFlow.getId());
+ Assert.assertEquals(flow.getEdges().size(), newFlow.getEdges().size());
+ Assert.assertEquals(flow.getNodes().size(), newFlow.getNodes().size());
+
+ List<Flow> flows = loader.fetchAllProjectFlows(project);
+ Assert.assertTrue(flows.size() == 1);
+ }
+
+ @Ignore @Test
+ public void testProjectProperties() throws ProjectManagerException {
+ ProjectLoader loader = createLoader();
+ ((JdbcProjectLoader) loader)
+ .setDefaultEncodingType(JdbcProjectLoader.EncodingType.PLAIN);
+ String projectName = "testProjectProperties";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+ project.setVersion(5);
+ Props props = new Props();
+ props.put("a", "abc");
+ props.put("b", "bcd");
+ props.put("c", "cde");
+ props.setSource("mysource");
+ loader.uploadProjectProperty(project, props);
+
+ Props retProps = loader.fetchProjectProperty(project, "mysource");
+
+ Assert.assertEquals(retProps.getSource(), props.getSource());
+ Assert.assertEquals(retProps.getKeySet(), props.getKeySet());
+ Assert.assertEquals(PropsUtils.toStringMap(retProps, true),
+ PropsUtils.toStringMap(props, true));
+ }
+
+ @Test
+ public void testProjectFilesUpload() throws ProjectManagerException {
+ if (!isTestSetup()) {
+ return;
+ }
+
+ ProjectLoader loader = createLoader();
+ String projectName = "testProjectFilesUpload1";
+ String projectDescription = "This is my new project";
+ User user = new User("testUser");
+
+ Project project =
+ loader.createNewProject(projectName, projectDescription, user);
+ Assert.assertTrue("Project Id set", project.getId() > -1);
+ Assert.assertEquals("Project name", projectName, project.getName());
+ Assert.assertEquals("Project description", projectDescription,
+ project.getDescription());
+
+ File testDir = new File("unit/project/testjob/testjob.zip");
+
+ loader.uploadProjectFile(project, 1, "zip", "testjob.zip", testDir,
+ user.getUserId());
+
+ ProjectFileHandler handler = loader.getUploadedFile(project, 1);
+ Assert.assertEquals(handler.getProjectId(), project.getId());
+ Assert.assertEquals(handler.getFileName(), "testjob.zip");
+ Assert.assertEquals(handler.getVersion(), 1);
+ Assert.assertEquals(handler.getFileType(), "zip");
+ File file = handler.getLocalFile();
+ Assert.assertTrue(handler.getLocalFile().exists());
+ Assert.assertEquals(handler.getFileName(), "testjob.zip");
+ Assert.assertEquals(handler.getUploader(), user.getUserId());
+
+ handler.deleteLocalFile();
+ Assert.assertTrue(handler.getLocalFile() == null);
+ Assert.assertFalse(file.exists());
+ }
+
+ // Custom equals for what I think is important
+ private void assertProjectMemberEquals(Project p1, Project p2) {
+ Assert.assertEquals(p1.getId(), p2.getId());
+ Assert.assertEquals(p1.getName(), p2.getName());
+ Assert.assertEquals(p1.getCreateTimestamp(), p2.getCreateTimestamp());
+ Assert.assertEquals(p1.getDescription(), p2.getDescription());
+ Assert.assertEquals(p1.getLastModifiedUser(), p2.getLastModifiedUser());
+ Assert.assertEquals(p1.getVersion(), p2.getVersion());
+ Assert.assertEquals(p1.isActive(), p2.isActive());
+ Assert.assertEquals(p1.getLastModifiedUser(), p2.getLastModifiedUser());
+
+ assertUserPermissionsEqual(p1, p2);
+ assertGroupPermissionsEqual(p1, p2);
+ }
+
+ private void assertUserPermissionsEqual(Project p1, Project p2) {
+ List<Pair<String, Permission>> perm1 = p1.getUserPermissions();
+ List<Pair<String, Permission>> perm2 = p2.getUserPermissions();
+
+ Assert.assertEquals(perm1.size(), perm2.size());
+
+ {
+ HashMap<String, Permission> perm1Map = new HashMap<String, Permission>();
+ for (Pair<String, Permission> p : perm1) {
+ perm1Map.put(p.getFirst(), p.getSecond());
+ }
+ for (Pair<String, Permission> p : perm2) {
+ Assert.assertTrue(perm1Map.containsKey(p.getFirst()));
+ Permission perm = perm1Map.get(p.getFirst());
+ Assert.assertEquals(perm, p.getSecond());
+ }
+ }
+
+ {
+ HashMap<String, Permission> perm2Map = new HashMap<String, Permission>();
+ for (Pair<String, Permission> p : perm2) {
+ perm2Map.put(p.getFirst(), p.getSecond());
+ }
+ for (Pair<String, Permission> p : perm1) {
+ Assert.assertTrue(perm2Map.containsKey(p.getFirst()));
+ Permission perm = perm2Map.get(p.getFirst());
+ Assert.assertEquals(perm, p.getSecond());
+ }
+ }
+ }
+
+ private void assertGroupPermissionsEqual(Project p1, Project p2) {
+ List<Pair<String, Permission>> perm1 = p1.getGroupPermissions();
+ List<Pair<String, Permission>> perm2 = p2.getGroupPermissions();
+
+ Assert.assertEquals(perm1.size(), perm2.size());
+
+ {
+ HashMap<String, Permission> perm1Map = new HashMap<String, Permission>();
+ for (Pair<String, Permission> p : perm1) {
+ perm1Map.put(p.getFirst(), p.getSecond());
+ }
+ for (Pair<String, Permission> p : perm2) {
+ Assert.assertTrue(perm1Map.containsKey(p.getFirst()));
+ Permission perm = perm1Map.get(p.getFirst());
+ Assert.assertEquals(perm, p.getSecond());
+ }
+ }
+
+ {
+ HashMap<String, Permission> perm2Map = new HashMap<String, Permission>();
+ for (Pair<String, Permission> p : perm2) {
+ perm2Map.put(p.getFirst(), p.getSecond());
+ }
+ for (Pair<String, Permission> p : perm1) {
+ Assert.assertTrue(perm2Map.containsKey(p.getFirst()));
+ Permission perm = perm2Map.get(p.getFirst());
+ Assert.assertEquals(perm, p.getSecond());
+ }
+ }
+ }
+
+ private ProjectLoader createLoader() {
+ Props props = new Props();
+ props.put("database.type", "mysql");
+
+ props.put("mysql.host", host);
+ props.put("mysql.port", port);
+ props.put("mysql.user", user);
+ props.put("mysql.database", database);
+ props.put("mysql.password", password);
+ props.put("mysql.numconnections", numConnections);
+
+ return new JdbcProjectLoader(props);
+ }
+
+ private boolean isTestSetup() {
+ if (!testDBExists) {
+ System.err.println("Skipping DB test because Db not setup.");
+ return false;
+ }
+
+ System.out.println("Running DB test because Db setup.");
+ return true;
+ }
+
+ public static class CountHandler implements ResultSetHandler<Integer> {
+ @Override
+ public Integer handle(ResultSet rs) throws SQLException {
+ int val = 0;
+ while (rs.next()) {
+ val++;
+ }
+
+ return val;
+ }
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/project/MockProjectLoader.java b/azkaban-common/src/test/java/azkaban/project/MockProjectLoader.java
new file mode 100644
index 0000000..909b82e
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/project/MockProjectLoader.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2014 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.project;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import azkaban.project.ProjectLogEvent.EventType;
+import azkaban.flow.Flow;
+import azkaban.user.Permission;
+import azkaban.user.User;
+import azkaban.utils.Props;
+import azkaban.utils.Triple;
+
+public class MockProjectLoader implements ProjectLoader {
+ public File dir;
+
+ public MockProjectLoader(File dir) {
+ this.dir = dir;
+ }
+
+ @Override
+ public List<Project> fetchAllActiveProjects() throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Project fetchProjectById(int id) throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Project createNewProject(String name, String description, User creator)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void removeProject(Project project, String user)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void updatePermission(Project project, String name, Permission perm,
+ boolean isGroup) throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void updateDescription(Project project, String description, String user)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public boolean postEvent(Project project, EventType type, String user,
+ String message) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public List<ProjectLogEvent> getProjectEvents(Project project, int num,
+ int skip) throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void uploadProjectFile(Project project, int version, String filetype,
+ String filename, File localFile, String user)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public ProjectFileHandler getUploadedFile(Project project, int version)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public ProjectFileHandler getUploadedFile(int projectId, int version)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void changeProjectVersion(Project project, int version, String user)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void uploadFlows(Project project, int version, Collection<Flow> flows)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void uploadFlow(Project project, int version, Flow flow)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public Flow fetchFlow(Project project, String flowId)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<Flow> fetchAllProjectFlows(Project project)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int getLatestProjectVersion(Project project)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public void uploadProjectProperty(Project project, Props props)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void uploadProjectProperties(Project project, List<Props> properties)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public Props fetchProjectProperty(Project project, String propsName)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Map<String, Props> fetchProjectProperties(int projectId, int version)
+ throws ProjectManagerException {
+ Map<String, Props> propertyMap = new HashMap<String, Props>();
+ for (File file : dir.listFiles()) {
+ String name = file.getName();
+ if (name.endsWith(".job") || name.endsWith(".properties")) {
+ try {
+ Props props = new Props(null, file);
+ propertyMap.put(name, props);
+ } catch (IOException e) {
+ throw new ProjectManagerException(e.getMessage());
+ }
+ }
+ }
+
+ return propertyMap;
+ }
+
+ @Override
+ public void cleanOlderProjectVersion(int projectId, int version)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void removePermission(Project project, String name, boolean isGroup)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void updateProjectProperty(Project project, Props props)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public Props fetchProjectProperty(int projectId, int projectVer,
+ String propsName) throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<Triple<String, Boolean, Permission>> getProjectPermissions(
+ int projectId) throws ProjectManagerException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void updateProjectSettings(Project project)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void updateFlow(Project project, int version, Flow flow)
+ throws ProjectManagerException {
+ // TODO Auto-generated method stub
+
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/project/ProjectTest.java b/azkaban-common/src/test/java/azkaban/project/ProjectTest.java
new file mode 100644
index 0000000..da36d46
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/project/ProjectTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014 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.project;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import azkaban.user.Permission;
+import azkaban.user.Permission.Type;
+import azkaban.utils.JSONUtils;
+
+public class ProjectTest {
+ @Test
+ public void testToAndFromObject() throws Exception {
+ Project project = new Project(1, "tesTing");
+ project.setCreateTimestamp(1l);
+ project.setLastModifiedTimestamp(2l);
+ project.setDescription("I am a test");
+ project.setUserPermission("user1", new Permission(new Type[] { Type.ADMIN,
+ Type.EXECUTE }));
+
+ Object obj = project.toObject();
+ String json = JSONUtils.toJSON(obj);
+
+ Object jsonObj = JSONUtils.parseJSONFromString(json);
+
+ Project parsedProject = Project.projectFromObject(jsonObj);
+
+ assertTrue(project.equals(parsedProject));
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java b/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
new file mode 100644
index 0000000..926f705
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.ReadablePeriod;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import azkaban.utils.Utils;
+import azkaban.trigger.builtin.BasicTimeChecker;
+
+public class BasicTimeCheckerTest {
+
+ @Test
+ public void basicTimerTest() {
+
+ Map<String, ConditionChecker> checkers =
+ new HashMap<String, ConditionChecker>();
+
+ // get a new timechecker, start from now, repeat every minute. should
+ // evaluate to false now, and true a minute later.
+ DateTime now = DateTime.now();
+ ReadablePeriod period = Utils.parsePeriodString("10s");
+
+ BasicTimeChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+ now.getZone(), true, true, period);
+ checkers.put(timeChecker.getId(), timeChecker);
+ String expr = timeChecker.getId() + ".eval()";
+
+ Condition cond = new Condition(checkers, expr);
+ System.out.println(expr);
+
+ assertFalse(cond.isMet());
+
+ // sleep for 1 min
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ assertTrue(cond.isMet());
+
+ cond.resetCheckers();
+
+ assertFalse(cond.isMet());
+
+ // sleep for 1 min
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ assertTrue(cond.isMet());
+
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java b/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
new file mode 100644
index 0000000..cfff784
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+import org.junit.Test;
+import org.junit.Ignore;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import azkaban.trigger.builtin.BasicTimeChecker;
+import azkaban.utils.JSONUtils;
+import azkaban.utils.Props;
+import azkaban.utils.Utils;
+
+public class ConditionTest {
+
+ @Test
+ public void conditionTest() {
+
+ Map<String, ConditionChecker> checkers =
+ new HashMap<String, ConditionChecker>();
+
+ ThresholdChecker fake1 = new ThresholdChecker("thresholdchecker1", 10);
+ ThresholdChecker fake2 = new ThresholdChecker("thresholdchecker2", 20);
+ ThresholdChecker.setVal(15);
+ checkers.put(fake1.getId(), fake1);
+ checkers.put(fake2.getId(), fake2);
+
+ String expr1 =
+ "( " + fake1.getId() + ".eval()" + " && " + fake2.getId() + ".eval()"
+ + " )" + " || " + "( " + fake1.getId() + ".eval()" + " && " + "!"
+ + fake2.getId() + ".eval()" + " )";
+ String expr2 =
+ "( " + fake1.getId() + ".eval()" + " && " + fake2.getId() + ".eval()"
+ + " )" + " || " + "( " + fake1.getId() + ".eval()" + " && "
+ + fake2.getId() + ".eval()" + " )";
+
+ Condition cond = new Condition(checkers, expr1);
+
+ System.out.println("Setting expression " + expr1);
+ assertTrue(cond.isMet());
+ cond.setExpression(expr2);
+ System.out.println("Setting expression " + expr2);
+ assertFalse(cond.isMet());
+
+ }
+
+ @Ignore @Test
+ public void jsonConversionTest() throws Exception {
+
+ CheckerTypeLoader checkerTypeLoader = new CheckerTypeLoader();
+ checkerTypeLoader.init(new Props());
+ Condition.setCheckerLoader(checkerTypeLoader);
+
+ Map<String, ConditionChecker> checkers =
+ new HashMap<String, ConditionChecker>();
+
+ // get a new timechecker, start from now, repeat every minute. should
+ // evaluate to false now, and true a minute later.
+ DateTime now = DateTime.now();
+ String period = "6s";
+
+ // BasicTimeChecker timeChecker = new BasicTimeChecker(now, true, true,
+ // period);
+ ConditionChecker timeChecker =
+ new BasicTimeChecker("BasicTimeChecker_1", now.getMillis(),
+ now.getZone(), true, true, Utils.parsePeriodString(period));
+ System.out.println("checker id is " + timeChecker.getId());
+
+ checkers.put(timeChecker.getId(), timeChecker);
+ String expr = timeChecker.getId() + ".eval()";
+
+ Condition cond = new Condition(checkers, expr);
+
+ File temp = File.createTempFile("temptest", "temptest");
+ temp.deleteOnExit();
+ Object obj = cond.toJson();
+ JSONUtils.toJSON(obj, temp);
+
+ Condition cond2 = Condition.fromJson(JSONUtils.parseJSONFromFile(temp));
+
+ Map<String, ConditionChecker> checkers2 = cond2.getCheckers();
+
+ assertTrue(cond.getExpression().equals(cond2.getExpression()));
+ System.out.println("cond1: " + cond.getExpression());
+ System.out.println("cond2: " + cond2.getExpression());
+ assertTrue(checkers2.size() == 1);
+ ConditionChecker checker2 = checkers2.get(timeChecker.getId());
+ // assertTrue(checker2.getId().equals(timeChecker.getId()));
+ System.out.println("checker1: " + timeChecker.getId());
+ System.out.println("checker2: " + checker2.getId());
+ assertTrue(timeChecker.getId().equals(checker2.getId()));
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/DummyTriggerAction.java b/azkaban-common/src/test/java/azkaban/trigger/DummyTriggerAction.java
new file mode 100644
index 0000000..c5a26c4
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/DummyTriggerAction.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.Map;
+
+import azkaban.trigger.TriggerAction;
+
+public class DummyTriggerAction implements TriggerAction {
+
+ public static final String type = "DummyAction";
+
+ private String message;
+
+ public DummyTriggerAction(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public TriggerAction fromJson(Object obj) {
+ return null;
+ }
+
+ @Override
+ public Object toJson() {
+ return null;
+ }
+
+ @Override
+ public void doAction() {
+ System.out.println(getType() + " invoked.");
+ System.out.println(message);
+ }
+
+ @Override
+ public String getDescription() {
+ return "this is real dummy action";
+ }
+
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ @Override
+ public void setContext(Map<String, Object> context) {
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/ExecuteFlowActionTest.java b/azkaban-common/src/test/java/azkaban/trigger/ExecuteFlowActionTest.java
new file mode 100644
index 0000000..0e8b2f9
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/ExecuteFlowActionTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import azkaban.executor.ExecutionOptions;
+import azkaban.trigger.builtin.ExecuteFlowAction;
+import azkaban.utils.Props;
+
+public class ExecuteFlowActionTest {
+
+ @Ignore @Test
+ public void jsonConversionTest() throws Exception {
+ ActionTypeLoader loader = new ActionTypeLoader();
+ loader.init(new Props());
+
+ ExecutionOptions options = new ExecutionOptions();
+ List<Object> disabledJobs = new ArrayList<Object>();
+ options.setDisabledJobs(disabledJobs);
+
+ ExecuteFlowAction executeFlowAction =
+ new ExecuteFlowAction("ExecuteFlowAction", 1, "testproject",
+ "testflow", "azkaban", options, null);
+
+ Object obj = executeFlowAction.toJson();
+
+ ExecuteFlowAction action =
+ (ExecuteFlowAction) loader.createActionFromJson(ExecuteFlowAction.type,
+ obj);
+ assertTrue(executeFlowAction.getProjectId() == action.getProjectId());
+ assertTrue(executeFlowAction.getFlowName().equals(action.getFlowName()));
+ assertTrue(executeFlowAction.getSubmitUser().equals(action.getSubmitUser()));
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java b/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
new file mode 100644
index 0000000..ecbe990
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.dbutils.DbUtils;
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.dbutils.ResultSetHandler;
+
+import org.joda.time.DateTime;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import azkaban.database.DataSourceUtils;
+import azkaban.executor.ExecutionOptions;
+import azkaban.trigger.builtin.BasicTimeChecker;
+import azkaban.trigger.builtin.ExecuteFlowAction;
+import azkaban.utils.Props;
+import azkaban.utils.Utils;
+
+public class JdbcTriggerLoaderTest {
+
+ private static boolean testDBExists = false;
+ // @TODO remove this and turn into local host.
+ private static final String host = "localhost";
+ private static final int port = 3306;
+ private static final String database = "azkaban2";
+ private static final String user = "azkaban";
+ private static final String password = "azkaban";
+ private static final int numConnections = 10;
+
+ private TriggerLoader loader;
+ private CheckerTypeLoader checkerLoader;
+ private ActionTypeLoader actionLoader;
+
+ @Before
+ public void setup() throws TriggerException {
+ Props props = new Props();
+ props.put("database.type", "mysql");
+
+ props.put("mysql.host", host);
+ props.put("mysql.port", port);
+ props.put("mysql.user", user);
+ props.put("mysql.database", database);
+ props.put("mysql.password", password);
+ props.put("mysql.numconnections", numConnections);
+
+ loader = new JdbcTriggerLoader(props);
+ checkerLoader = new CheckerTypeLoader();
+ checkerLoader.init(new Props());
+ Condition.setCheckerLoader(checkerLoader);
+ actionLoader = new ActionTypeLoader();
+ actionLoader.init(new Props());
+ Trigger.setActionTypeLoader(actionLoader);
+ setupDB();
+ }
+
+ public void setupDB() {
+ DataSource dataSource =
+ DataSourceUtils.getMySQLDataSource(host, port, database, user,
+ password, numConnections);
+ testDBExists = true;
+
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ CountHandler countHandler = new CountHandler();
+ QueryRunner runner = new QueryRunner();
+ try {
+ runner.query(connection, "SELECT COUNT(1) FROM triggers", countHandler);
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ DbUtils.closeQuietly(connection);
+
+ clearDB();
+ }
+
+ @After
+ public void clearDB() {
+ if (!testDBExists) {
+ return;
+ }
+
+ DataSource dataSource =
+ DataSourceUtils.getMySQLDataSource(host, port, database, user,
+ password, numConnections);
+ Connection connection = null;
+ try {
+ connection = dataSource.getConnection();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ QueryRunner runner = new QueryRunner();
+ try {
+ runner.update(connection, "DELETE FROM triggers");
+
+ } catch (SQLException e) {
+ e.printStackTrace();
+ testDBExists = false;
+ DbUtils.closeQuietly(connection);
+ return;
+ }
+
+ DbUtils.closeQuietly(connection);
+ }
+
+ @Ignore @Test
+ public void addTriggerTest() throws TriggerLoaderException {
+ Trigger t1 = createTrigger("testProj1", "testFlow1", "source1");
+ Trigger t2 = createTrigger("testProj2", "testFlow2", "source2");
+ loader.addTrigger(t1);
+ List<Trigger> ts = loader.loadTriggers();
+ assertTrue(ts.size() == 1);
+
+ Trigger t3 = ts.get(0);
+ assertTrue(t3.getSource().equals("source1"));
+
+ loader.addTrigger(t2);
+ ts = loader.loadTriggers();
+ assertTrue(ts.size() == 2);
+
+ for (Trigger t : ts) {
+ if (t.getTriggerId() == t2.getTriggerId()) {
+ t.getSource().equals(t2.getSource());
+ }
+ }
+ }
+
+ @Ignore @Test
+ public void removeTriggerTest() throws TriggerLoaderException {
+ Trigger t1 = createTrigger("testProj1", "testFlow1", "source1");
+ Trigger t2 = createTrigger("testProj2", "testFlow2", "source2");
+ loader.addTrigger(t1);
+ loader.addTrigger(t2);
+ List<Trigger> ts = loader.loadTriggers();
+ assertTrue(ts.size() == 2);
+ loader.removeTrigger(t2);
+ ts = loader.loadTriggers();
+ assertTrue(ts.size() == 1);
+ assertTrue(ts.get(0).getTriggerId() == t1.getTriggerId());
+ }
+
+ @Ignore @Test
+ public void updateTriggerTest() throws TriggerLoaderException {
+ Trigger t1 = createTrigger("testProj1", "testFlow1", "source1");
+ t1.setResetOnExpire(true);
+ loader.addTrigger(t1);
+ List<Trigger> ts = loader.loadTriggers();
+ assertTrue(ts.get(0).isResetOnExpire() == true);
+ t1.setResetOnExpire(false);
+ loader.updateTrigger(t1);
+ ts = loader.loadTriggers();
+ assertTrue(ts.get(0).isResetOnExpire() == false);
+ }
+
+ private Trigger createTrigger(String projName, String flowName, String source) {
+ DateTime now = DateTime.now();
+ ConditionChecker checker1 =
+ new BasicTimeChecker("timeChecker1", now.getMillis(), now.getZone(),
+ true, true, Utils.parsePeriodString("1h"));
+ Map<String, ConditionChecker> checkers1 =
+ new HashMap<String, ConditionChecker>();
+ checkers1.put(checker1.getId(), checker1);
+ String expr1 = checker1.getId() + ".eval()";
+ Condition triggerCond = new Condition(checkers1, expr1);
+ Condition expireCond = new Condition(checkers1, expr1);
+ List<TriggerAction> actions = new ArrayList<TriggerAction>();
+ TriggerAction action =
+ new ExecuteFlowAction("executeAction", 1, projName, flowName,
+ "azkaban", new ExecutionOptions(), null);
+ actions.add(action);
+ Trigger t =
+ new Trigger(now.getMillis(), now.getMillis(), "azkaban", source,
+ triggerCond, expireCond, actions);
+ return t;
+ }
+
+ public static class CountHandler implements ResultSetHandler<Integer> {
+ @Override
+ public Integer handle(ResultSet rs) throws SQLException {
+ int val = 0;
+ while (rs.next()) {
+ val++;
+ }
+
+ return val;
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/MockTriggerLoader.java b/azkaban-common/src/test/java/azkaban/trigger/MockTriggerLoader.java
new file mode 100644
index 0000000..7127cec
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/MockTriggerLoader.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MockTriggerLoader implements TriggerLoader {
+
+ Map<Integer, Trigger> triggers = new HashMap<Integer, Trigger>();
+ int triggerCount = 0;
+
+ @Override
+ public synchronized void addTrigger(Trigger t) throws TriggerLoaderException {
+ t.setTriggerId(triggerCount);
+ t.setLastModifyTime(System.currentTimeMillis());
+ triggers.put(t.getTriggerId(), t);
+ triggerCount++;
+ }
+
+ @Override
+ public synchronized void removeTrigger(Trigger s)
+ throws TriggerLoaderException {
+ triggers.remove(s);
+ }
+
+ @Override
+ public synchronized void updateTrigger(Trigger t)
+ throws TriggerLoaderException {
+ t.setLastModifyTime(System.currentTimeMillis());
+ triggers.put(t.getTriggerId(), t);
+ }
+
+ @Override
+ public synchronized List<Trigger> loadTriggers()
+ throws TriggerLoaderException {
+ return new ArrayList<Trigger>(triggers.values());
+ }
+
+ @Override
+ public synchronized Trigger loadTrigger(int triggerId)
+ throws TriggerLoaderException {
+ return triggers.get(triggerId);
+ }
+
+ @Override
+ public List<Trigger> getUpdatedTriggers(long lastUpdateTime)
+ throws TriggerLoaderException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/ThresholdChecker.java b/azkaban-common/src/test/java/azkaban/trigger/ThresholdChecker.java
new file mode 100644
index 0000000..279c329
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/ThresholdChecker.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.Map;
+
+import azkaban.trigger.ConditionChecker;
+
+public class ThresholdChecker implements ConditionChecker {
+
+ private int threshold = -1;
+
+ private static int curVal = -1;
+
+ public static final String type = "ThresholdChecker";
+
+ private String id;
+
+ private boolean checkerMet = false;
+ private boolean checkerReset = false;
+
+ public ThresholdChecker(String id, int threshold) {
+ this.id = id;
+ this.threshold = threshold;
+ }
+
+ public synchronized static void setVal(int val) {
+ curVal = val;
+ }
+
+ @Override
+ public Boolean eval() {
+ if (curVal > threshold) {
+ checkerMet = true;
+ }
+ return checkerMet;
+ }
+
+ public boolean isCheckerMet() {
+ return checkerMet;
+ }
+
+ @Override
+ public void reset() {
+ checkerMet = false;
+ checkerReset = true;
+ }
+
+ public boolean isCheckerReset() {
+ return checkerReset;
+ }
+
+ /*
+ * TimeChecker format:
+ * type_first-time-in-millis_next-time-in-millis_timezone_is
+ * -recurring_skip-past-checks_period
+ */
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public ConditionChecker fromJson(Object obj) {
+ return null;
+ }
+
+ @Override
+ public Object getNum() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Object toJson() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void stopChecker() {
+ return;
+
+ }
+
+ @Override
+ public void setContext(Map<String, Object> context) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public long getNextCheckTime() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/TriggerManagerDeadlockTest.java b/azkaban-common/src/test/java/azkaban/trigger/TriggerManagerDeadlockTest.java
new file mode 100644
index 0000000..a2f672e
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/TriggerManagerDeadlockTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import azkaban.alert.Alerter;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.ExecutorManager;
+import azkaban.executor.ExecutorManagerException;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.trigger.builtin.CreateTriggerAction;
+import azkaban.utils.Props;
+
+public class TriggerManagerDeadlockTest {
+
+ TriggerLoader loader;
+ TriggerManager triggerManager;
+ ExecutorLoader execLoader;
+
+ @Before
+ public void setup() throws ExecutorManagerException, TriggerManagerException {
+ loader = new MockTriggerLoader();
+ Props props = new Props();
+ props.put("trigger.scan.interval", 1000);
+ props.put("executor.port", 12321);
+ execLoader = new MockExecutorLoader();
+ Map<String, Alerter> alerters = new HashMap<String, Alerter>();
+ ExecutorManager executorManager =
+ new ExecutorManager(props, execLoader, alerters);
+ triggerManager = new TriggerManager(props, loader, executorManager);
+ }
+
+ @After
+ public void tearDown() {
+
+ }
+
+ @Test
+ public void deadlockTest() throws TriggerLoaderException,
+ TriggerManagerException {
+ // this should well saturate it
+ for (int i = 0; i < 1000; i++) {
+ Trigger t = createSelfRegenTrigger();
+ loader.addTrigger(t);
+ }
+ // keep going and add more
+ for (int i = 0; i < 10000; i++) {
+ Trigger d = createDummyTrigger();
+ triggerManager.insertTrigger(d);
+ triggerManager.removeTrigger(d);
+ }
+
+ System.out.println("No dead lock.");
+ }
+
+ public class AlwaysOnChecker implements ConditionChecker {
+
+ public static final String type = "AlwaysOnChecker";
+
+ private final String id;
+ private final Boolean alwaysOn;
+
+ public AlwaysOnChecker(String id, Boolean alwaysOn) {
+ this.id = id;
+ this.alwaysOn = alwaysOn;
+ }
+
+ @Override
+ public Object eval() {
+ // TODO Auto-generated method stub
+ return alwaysOn;
+ }
+
+ @Override
+ public Object getNum() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void reset() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getType() {
+ // TODO Auto-generated method stub
+ return type;
+ }
+
+ @Override
+ public ConditionChecker fromJson(Object obj) throws Exception {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Object toJson() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void stopChecker() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void setContext(Map<String, Object> context) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public long getNextCheckTime() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ }
+
+ private Trigger createSelfRegenTrigger() {
+ ConditionChecker alwaysOnChecker =
+ new AlwaysOnChecker("alwaysOn", Boolean.TRUE);
+ String triggerExpr = alwaysOnChecker.getId() + ".eval()";
+ Map<String, ConditionChecker> triggerCheckers =
+ new HashMap<String, ConditionChecker>();
+ triggerCheckers.put(alwaysOnChecker.getId(), alwaysOnChecker);
+ Condition triggerCond = new Condition(triggerCheckers, triggerExpr);
+
+ TriggerAction triggerAct =
+ new CreateTriggerAction("dummyTrigger", createDummyTrigger());
+ List<TriggerAction> actions = new ArrayList<TriggerAction>();
+ actions.add(triggerAct);
+
+ ConditionChecker alwaysOffChecker =
+ new AlwaysOnChecker("alwaysOff", Boolean.FALSE);
+ String expireExpr = alwaysOffChecker.getId() + ".eval()";
+ Map<String, ConditionChecker> expireCheckers =
+ new HashMap<String, ConditionChecker>();
+ expireCheckers.put(alwaysOffChecker.getId(), alwaysOffChecker);
+ Condition expireCond = new Condition(expireCheckers, expireExpr);
+
+ Trigger t =
+ new Trigger("azkaban", "azkabanTest", triggerCond, expireCond, actions);
+ return t;
+ }
+
+ private Trigger createDummyTrigger() {
+ ConditionChecker alwaysOnChecker =
+ new AlwaysOnChecker("alwaysOn", Boolean.TRUE);
+ String triggerExpr = alwaysOnChecker.getId() + ".eval()";
+ Map<String, ConditionChecker> triggerCheckers =
+ new HashMap<String, ConditionChecker>();
+ triggerCheckers.put(alwaysOnChecker.getId(), alwaysOnChecker);
+ Condition triggerCond = new Condition(triggerCheckers, triggerExpr);
+
+ TriggerAction triggerAct = new DummyTriggerAction("howdy!");
+ List<TriggerAction> actions = new ArrayList<TriggerAction>();
+ actions.add(triggerAct);
+
+ ConditionChecker alwaysOffChecker =
+ new AlwaysOnChecker("alwaysOff", Boolean.FALSE);
+ String expireExpr = alwaysOffChecker.getId() + ".eval()";
+ Map<String, ConditionChecker> expireCheckers =
+ new HashMap<String, ConditionChecker>();
+ expireCheckers.put(alwaysOffChecker.getId(), alwaysOffChecker);
+ Condition expireCond = new Condition(expireCheckers, expireExpr);
+
+ Trigger t =
+ new Trigger("azkaban", "azkabanTest", triggerCond, expireCond, actions);
+ return t;
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/TriggerManagerTest.java b/azkaban-common/src/test/java/azkaban/trigger/TriggerManagerTest.java
new file mode 100644
index 0000000..4021cc7
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/TriggerManagerTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import azkaban.utils.Props;
+
+public class TriggerManagerTest {
+
+ private TriggerLoader triggerLoader;
+
+ @Before
+ public void setup() throws TriggerException, TriggerManagerException {
+ triggerLoader = new MockTriggerLoader();
+
+ }
+
+ @After
+ public void tearDown() {
+
+ }
+
+ @Ignore @Test
+ public void triggerManagerSimpleTest() throws TriggerManagerException {
+ Props props = new Props();
+ props.put("trigger.scan.interval", 4000);
+ TriggerManager triggerManager =
+ new TriggerManager(props, triggerLoader, null);
+
+ triggerManager.registerCheckerType(ThresholdChecker.type,
+ ThresholdChecker.class);
+ triggerManager.registerActionType(DummyTriggerAction.type,
+ DummyTriggerAction.class);
+
+ ThresholdChecker.setVal(1);
+
+ triggerManager.insertTrigger(
+ createDummyTrigger("test1", "triggerLoader", 10), "testUser");
+ List<Trigger> triggers = triggerManager.getTriggers();
+ assertTrue(triggers.size() == 1);
+ Trigger t1 = triggers.get(0);
+ t1.setResetOnTrigger(false);
+ triggerManager.updateTrigger(t1, "testUser");
+ ThresholdChecker checker1 =
+ (ThresholdChecker) t1.getTriggerCondition().getCheckers().values()
+ .toArray()[0];
+ assertTrue(t1.getSource().equals("triggerLoader"));
+
+ Trigger t2 =
+ createDummyTrigger("test2: add new trigger", "addNewTriggerTest", 20);
+ triggerManager.insertTrigger(t2, "testUser");
+ ThresholdChecker checker2 =
+ (ThresholdChecker) t2.getTriggerCondition().getCheckers().values()
+ .toArray()[0];
+
+ ThresholdChecker.setVal(15);
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ assertTrue(checker1.isCheckerMet() == false);
+ assertTrue(checker2.isCheckerMet() == false);
+ assertTrue(checker1.isCheckerReset() == false);
+ assertTrue(checker2.isCheckerReset() == false);
+
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ assertTrue(checker1.isCheckerMet() == true);
+ assertTrue(checker2.isCheckerMet() == false);
+ assertTrue(checker1.isCheckerReset() == false);
+ assertTrue(checker2.isCheckerReset() == false);
+
+ ThresholdChecker.setVal(25);
+ try {
+ Thread.sleep(4000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ assertTrue(checker1.isCheckerMet() == true);
+ assertTrue(checker1.isCheckerReset() == false);
+ assertTrue(checker2.isCheckerReset() == true);
+
+ triggers = triggerManager.getTriggers();
+ assertTrue(triggers.size() == 1);
+
+ }
+
+ public class MockTriggerLoader implements TriggerLoader {
+
+ private Map<Integer, Trigger> triggers = new HashMap<Integer, Trigger>();
+ private int idIndex = 0;
+
+ @Override
+ public void addTrigger(Trigger t) throws TriggerLoaderException {
+ t.setTriggerId(idIndex++);
+ triggers.put(t.getTriggerId(), t);
+ }
+
+ @Override
+ public void removeTrigger(Trigger s) throws TriggerLoaderException {
+ triggers.remove(s.getTriggerId());
+
+ }
+
+ @Override
+ public void updateTrigger(Trigger t) throws TriggerLoaderException {
+ triggers.put(t.getTriggerId(), t);
+ }
+
+ @Override
+ public List<Trigger> loadTriggers() {
+ return new ArrayList<Trigger>(triggers.values());
+ }
+
+ @Override
+ public Trigger loadTrigger(int triggerId) throws TriggerLoaderException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public List<Trigger> getUpdatedTriggers(long lastUpdateTime)
+ throws TriggerLoaderException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ }
+
+ private Trigger createDummyTrigger(String message, String source,
+ int threshold) {
+
+ Map<String, ConditionChecker> checkers =
+ new HashMap<String, ConditionChecker>();
+ ConditionChecker checker =
+ new ThresholdChecker(ThresholdChecker.type, threshold);
+ checkers.put(checker.getId(), checker);
+
+ List<TriggerAction> actions = new ArrayList<TriggerAction>();
+ TriggerAction act = new DummyTriggerAction(message);
+ actions.add(act);
+
+ String expr = checker.getId() + ".eval()";
+
+ Condition triggerCond = new Condition(checkers, expr);
+ Condition expireCond = new Condition(checkers, expr);
+
+ Trigger fakeTrigger =
+ new Trigger(DateTime.now().getMillis(), DateTime.now().getMillis(),
+ "azkaban", source, triggerCond, expireCond, actions);
+ fakeTrigger.setResetOnTrigger(true);
+ fakeTrigger.setResetOnExpire(true);
+
+ return fakeTrigger;
+ }
+
+ // public class MockCheckerLoader extends CheckerTypeLoader{
+ //
+ // @Override
+ // public void init(Props props) {
+ // checkerToClass.put(ThresholdChecker.type, ThresholdChecker.class);
+ // }
+ // }
+ //
+ // public class MockActionLoader extends ActionTypeLoader {
+ // @Override
+ // public void init(Props props) {
+ // actionToClass.put(DummyTriggerAction.type, DummyTriggerAction.class);
+ // }
+ // }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java b/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
new file mode 100644
index 0000000..a0e1f51
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2014 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.trigger;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import azkaban.executor.ExecutionOptions;
+import azkaban.trigger.builtin.BasicTimeChecker;
+import azkaban.trigger.builtin.ExecuteFlowAction;
+import azkaban.utils.JSONUtils;
+import azkaban.utils.Props;
+import azkaban.utils.Utils;
+
+public class TriggerTest {
+
+ private CheckerTypeLoader checkerLoader;
+ private ActionTypeLoader actionLoader;
+
+ @Before
+ public void setup() throws TriggerException {
+ checkerLoader = new CheckerTypeLoader();
+ checkerLoader.init(new Props());
+ Condition.setCheckerLoader(checkerLoader);
+ actionLoader = new ActionTypeLoader();
+ actionLoader.init(new Props());
+ Trigger.setActionTypeLoader(actionLoader);
+ }
+
+ @Ignore @Test
+ public void jsonConversionTest() throws Exception {
+ DateTime now = DateTime.now();
+ ConditionChecker checker1 =
+ new BasicTimeChecker("timeChecker1", now.getMillis(), now.getZone(),
+ true, true, Utils.parsePeriodString("1h"));
+ Map<String, ConditionChecker> checkers1 =
+ new HashMap<String, ConditionChecker>();
+ checkers1.put(checker1.getId(), checker1);
+ String expr1 = checker1.getId() + ".eval()";
+ Condition triggerCond = new Condition(checkers1, expr1);
+ Condition expireCond = new Condition(checkers1, expr1);
+ List<TriggerAction> actions = new ArrayList<TriggerAction>();
+ TriggerAction action =
+ new ExecuteFlowAction("executeAction", 1, "testProj", "testFlow",
+ "azkaban", new ExecutionOptions(), null);
+ actions.add(action);
+ Trigger t =
+ new Trigger(now.getMillis(), now.getMillis(), "azkaban", "test",
+ triggerCond, expireCond, actions);
+
+ File temp = File.createTempFile("temptest", "temptest");
+ temp.deleteOnExit();
+ Object obj = t.toJson();
+ JSONUtils.toJSON(obj, temp);
+
+ Trigger t2 = Trigger.fromJson(JSONUtils.parseJSONFromFile(temp));
+
+ assertTrue(t.getSource().equals(t2.getSource()));
+ assertTrue(t.getTriggerId() == t2.getTriggerId());
+
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/user/PermissionTest.java b/azkaban-common/src/test/java/azkaban/user/PermissionTest.java
new file mode 100644
index 0000000..e34a5b4
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/user/PermissionTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2014 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.user;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+import azkaban.user.Permission.Type;
+
+public class PermissionTest {
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testEmptyPermissionCreation() throws Exception {
+ Permission permission = new Permission();
+ permission.addPermissionsByName(new String[] {});
+ }
+
+ @Test
+ public void testSinglePermissionCreation() throws Exception {
+ Permission perm1 = new Permission();
+ perm1.addPermissionsByName("READ");
+
+ Permission perm2 = new Permission();
+ perm2.addPermission(Type.READ);
+ info("Compare " + perm1.toString() + " and " + perm2.toString());
+ assertTrue(perm1.equals(perm2));
+ }
+
+ @Test
+ public void testListPermissionCreation() throws Exception {
+ Permission perm1 = new Permission();
+ perm1.addPermissionsByName(new String[] { "READ", "EXECUTE" });
+
+ Permission perm2 = new Permission();
+ perm2.addPermission(new Type[] { Type.EXECUTE, Type.READ });
+ info("Compare " + perm1.toString() + " and " + perm2.toString());
+ assertTrue(perm1.equals(perm2));
+ }
+
+ @Test
+ public void testRemovePermission() throws Exception {
+ Permission perm1 = new Permission();
+ perm1.addPermissionsByName(new String[] { "READ", "EXECUTE", "WRITE" });
+ perm1.removePermissions(Type.EXECUTE);
+
+ Permission perm2 = new Permission();
+ perm2.addPermission(new Type[] { Type.READ, Type.WRITE });
+ info("Compare " + perm1.toString() + " and " + perm2.toString());
+ assertTrue(perm1.equals(perm2));
+ }
+
+ @Test
+ public void testRemovePermissionByName() throws Exception {
+ Permission perm1 = new Permission();
+ perm1.addPermissionsByName(new String[] { "READ", "EXECUTE", "WRITE" });
+ perm1.removePermissionsByName("EXECUTE");
+
+ Permission perm2 = new Permission();
+ perm2.addPermission(new Type[] { Type.READ, Type.WRITE });
+ info("Compare " + perm1.toString() + " and " + perm2.toString());
+ assertTrue(perm1.equals(perm2));
+ }
+
+ @Test
+ public void testToAndFromObject() throws Exception {
+ Permission permission = new Permission();
+ permission
+ .addPermissionsByName(new String[] { "READ", "EXECUTE", "WRITE" });
+
+ String[] array = permission.toStringArray();
+ Permission permission2 = new Permission();
+ permission2.addPermissionsByName(array);
+ assertTrue(permission.equals(permission2));
+ }
+
+ @Test
+ public void testFlags() throws Exception {
+ Permission permission = new Permission();
+ permission.addPermission(new Type[] { Type.READ, Type.WRITE });
+
+ int flags = permission.toFlags();
+ Permission permission2 = new Permission(flags);
+
+ assertTrue(permission2.isPermissionSet(Type.READ));
+ assertTrue(permission2.isPermissionSet(Type.WRITE));
+
+ assertTrue(permission.equals(permission2));
+ }
+
+ /**
+ * Why? because it's quicker.
+ *
+ * @param message
+ */
+ public void info(String message) {
+ System.out.println(message);
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/user/XmlUserManagerTest.java b/azkaban-common/src/test/java/azkaban/user/XmlUserManagerTest.java
new file mode 100644
index 0000000..d5d93c2
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/user/XmlUserManagerTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2014 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.user;
+
+import java.util.HashSet;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+import azkaban.utils.Props;
+import azkaban.utils.UndefinedPropertyException;
+
+public class XmlUserManagerTest {
+ private Props baseProps = new Props();
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ /**
+ * Testing for when the xml path isn't set in properties.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testFilePropNotSet() throws Exception {
+ Props props = new Props(baseProps);
+
+ // Should throw
+ try {
+ @SuppressWarnings("unused")
+ XmlUserManager manager = new XmlUserManager(props);
+ } catch (UndefinedPropertyException e) {
+ return;
+ }
+
+ fail("XmlUserManager should throw an exception when the file property isn't set");
+ }
+
+ /**
+ * Testing for when the xml path doesn't exist.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testDoNotExist() throws Exception {
+ Props props = new Props(baseProps);
+ props.put(XmlUserManager.XML_FILE_PARAM, "unit/test-conf/doNotExist.xml");
+
+ try {
+ @SuppressWarnings("unused")
+ UserManager manager = new XmlUserManager(props);
+ } catch (RuntimeException e) {
+ return;
+ }
+
+ fail("XmlUserManager should throw an exception when the file doesn't exist");
+ }
+
+ @Ignore @Test
+ public void testBasicLoad() throws Exception {
+ Props props = new Props(baseProps);
+ props.put(XmlUserManager.XML_FILE_PARAM,
+ "unit/test-conf/azkaban-users-test1.xml");
+
+ UserManager manager = null;
+ try {
+ manager = new XmlUserManager(props);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ fail("XmlUserManager should've found file azkaban-users.xml");
+ }
+
+ try {
+ manager.getUser("user0", null);
+ } catch (UserManagerException e) {
+ System.out.println("Exception handled correctly: " + e.getMessage());
+ }
+
+ try {
+ manager.getUser(null, "etw");
+ } catch (UserManagerException e) {
+ System.out.println("Exception handled correctly: " + e.getMessage());
+ }
+
+ try {
+ manager.getUser("user0", "user0");
+ } catch (UserManagerException e) {
+ System.out.println("Exception handled correctly: " + e.getMessage());
+ }
+
+ try {
+ manager.getUser("user0", "password0");
+ } catch (UserManagerException e) {
+ e.printStackTrace();
+ fail("XmlUserManager should've returned a user.");
+ }
+
+ User user0 = manager.getUser("user0", "password0");
+ checkUser(user0, "role0", "group0");
+
+ User user1 = manager.getUser("user1", "password1");
+ checkUser(user1, "role0,role1", "group1,group2");
+
+ User user2 = manager.getUser("user2", "password2");
+ checkUser(user2, "role0,role1,role2", "group1,group2,group3");
+
+ User user3 = manager.getUser("user3", "password3");
+ checkUser(user3, "role1,role2", "group1,group2");
+
+ User user4 = manager.getUser("user4", "password4");
+ checkUser(user4, "role1,role2", "group1,group2");
+
+ User user5 = manager.getUser("user5", "password5");
+ checkUser(user5, "role1,role2", "group1,group2");
+
+ User user6 = manager.getUser("user6", "password6");
+ checkUser(user6, "role3,role2", "group1,group2");
+
+ User user7 = manager.getUser("user7", "password7");
+ checkUser(user7, "", "group1");
+
+ User user8 = manager.getUser("user8", "password8");
+ checkUser(user8, "role3", "");
+
+ User user9 = manager.getUser("user9", "password9");
+ checkUser(user9, "", "");
+ }
+
+ private void checkUser(User user, String rolesStr, String groupsStr) {
+ // Validating roles
+ HashSet<String> roleSet = new HashSet<String>(user.getRoles());
+ if (rolesStr.isEmpty()) {
+ if (!roleSet.isEmpty()) {
+ String outputRoleStr = "";
+ for (String role : roleSet) {
+ outputRoleStr += role + ",";
+ }
+ throw new RuntimeException("Roles mismatch for " + user.getUserId()
+ + ". Expected roles to be empty but got " + outputRoleStr);
+ }
+ } else {
+ String outputRoleStr = "";
+ for (String role : roleSet) {
+ outputRoleStr += role + ",";
+ }
+
+ String[] splitRoles = rolesStr.split(",");
+ HashSet<String> expectedRoles = new HashSet<String>();
+ for (String role : splitRoles) {
+ if (!roleSet.contains(role)) {
+ throw new RuntimeException("Roles mismatch for user "
+ + user.getUserId() + " role " + role + ". Expected roles to "
+ + rolesStr + " but got " + outputRoleStr);
+ }
+ expectedRoles.add(role);
+ }
+
+ for (String role : roleSet) {
+ if (!expectedRoles.contains(role)) {
+ throw new RuntimeException("Roles mismatch for user "
+ + user.getUserId() + " role " + role + ". Expected roles to "
+ + rolesStr + " but got " + outputRoleStr);
+ }
+ }
+ }
+
+ HashSet<String> groupSet = new HashSet<String>(user.getGroups());
+ if (groupsStr.isEmpty()) {
+ if (!groupSet.isEmpty()) {
+ String outputGroupStr = "";
+ for (String role : roleSet) {
+ outputGroupStr += role + ",";
+ }
+ throw new RuntimeException("Roles mismatch for " + user.getUserId()
+ + ". Expected roles to be empty but got " + outputGroupStr);
+ }
+ } else {
+ String outputGroupStr = "";
+ for (String group : groupSet) {
+ outputGroupStr += group + ",";
+ }
+
+ String[] splitGroups = groupsStr.split(",");
+ HashSet<String> expectedGroups = new HashSet<String>();
+ for (String group : splitGroups) {
+ if (!groupSet.contains(group)) {
+ throw new RuntimeException("Groups mismatch for user "
+ + user.getUserId() + " group " + group + ". Expected groups to "
+ + groupsStr + " but got " + outputGroupStr);
+ }
+ expectedGroups.add(group);
+ }
+
+ for (String group : groupSet) {
+ if (!expectedGroups.contains(group)) {
+ throw new RuntimeException("Groups mismatch for user "
+ + user.getUserId() + " group " + group + ". Expected groups to "
+ + groupsStr + " but got " + outputGroupStr);
+ }
+ }
+ }
+
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/utils/cache/CacheTest.java b/azkaban-common/src/test/java/azkaban/utils/cache/CacheTest.java
new file mode 100644
index 0000000..905cf5e
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/cache/CacheTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2014 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.utils.cache;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import azkaban.utils.cache.Cache.EjectionPolicy;
+
+public class CacheTest {
+ @Test
+ public void testLRU() {
+ CacheManager manager = CacheManager.getInstance();
+ Cache cache = manager.createCache();
+ cache.setEjectionPolicy(EjectionPolicy.LRU);
+ cache.setMaxCacheSize(4);
+
+ cache.insertElement("key1", "val1");
+ cache.insertElement("key2", "val2");
+ cache.insertElement("key3", "val3");
+ cache.insertElement("key4", "val4");
+
+ Assert.assertEquals(cache.get("key2"), "val2");
+ Assert.assertEquals(cache.get("key3"), "val3");
+ Assert.assertEquals(cache.get("key4"), "val4");
+ Assert.assertEquals(cache.get("key1"), "val1");
+ Assert.assertEquals(4, cache.getSize());
+
+ cache.insertElement("key5", "val5");
+ Assert.assertEquals(4, cache.getSize());
+ Assert.assertEquals(cache.get("key3"), "val3");
+ Assert.assertEquals(cache.get("key4"), "val4");
+ Assert.assertEquals(cache.get("key1"), "val1");
+ Assert.assertEquals(cache.get("key5"), "val5");
+ Assert.assertNull(cache.get("key2"));
+ }
+
+ @Test
+ public void testFIFO() {
+ CacheManager manager = CacheManager.getInstance();
+ Cache cache = manager.createCache();
+ cache.setEjectionPolicy(EjectionPolicy.FIFO);
+ cache.setMaxCacheSize(4);
+
+ cache.insertElement("key1", "val1");
+ synchronized (this) {
+ try {
+ wait(10);
+ } catch (InterruptedException e) {
+ }
+ }
+ cache.insertElement("key2", "val2");
+ cache.insertElement("key3", "val3");
+ cache.insertElement("key4", "val4");
+
+ Assert.assertEquals(cache.get("key2"), "val2");
+ Assert.assertEquals(cache.get("key3"), "val3");
+ Assert.assertEquals(cache.get("key4"), "val4");
+ Assert.assertEquals(cache.get("key1"), "val1");
+ Assert.assertEquals(4, cache.getSize());
+
+ cache.insertElement("key5", "val5");
+ Assert.assertEquals(4, cache.getSize());
+ Assert.assertEquals(cache.get("key3"), "val3");
+ Assert.assertEquals(cache.get("key4"), "val4");
+ Assert.assertEquals(cache.get("key2"), "val2");
+ Assert.assertEquals(cache.get("key5"), "val5");
+ Assert.assertNull(cache.get("key1"));
+ }
+
+ @Test
+ public void testTimeToLiveExpiry() {
+ CacheManager.setUpdateFrequency(200);
+ CacheManager manager = CacheManager.getInstance();
+ Cache cache = manager.createCache();
+
+ cache.setUpdateFrequencyMs(200);
+ cache.setEjectionPolicy(EjectionPolicy.FIFO);
+ cache.setExpiryTimeToLiveMs(4500);
+ cache.insertElement("key1", "val1");
+
+ synchronized (this) {
+ try {
+ wait(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+ Assert.assertEquals(cache.get("key1"), "val1");
+ cache.insertElement("key2", "val2");
+ synchronized (this) {
+ try {
+ wait(4000);
+ } catch (InterruptedException e) {
+ }
+ }
+ Assert.assertNull(cache.get("key1"));
+ Assert.assertEquals("val2", cache.get("key2"));
+
+ synchronized (this) {
+ try {
+ wait(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ Assert.assertNull(cache.get("key2"));
+ }
+
+ @Test
+ public void testIdleExpireExpiry() {
+ CacheManager.setUpdateFrequency(250);
+ CacheManager manager = CacheManager.getInstance();
+ Cache cache = manager.createCache();
+
+ cache.setUpdateFrequencyMs(250);
+ cache.setEjectionPolicy(EjectionPolicy.FIFO);
+ cache.setExpiryIdleTimeMs(4500);
+ cache.insertElement("key1", "val1");
+ cache.insertElement("key3", "val3");
+ synchronized (this) {
+ try {
+ wait(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+ Assert.assertEquals(cache.get("key1"), "val1");
+ cache.insertElement("key2", "val2");
+ synchronized (this) {
+ try {
+ wait(4000);
+ } catch (InterruptedException e) {
+ }
+ }
+ Assert.assertEquals("val1", cache.get("key1"));
+ Assert.assertNull(cache.get("key3"));
+ synchronized (this) {
+ try {
+ wait(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ Assert.assertNull(cache.get("key2"));
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/utils/DirectoryFlowLoaderTest.java b/azkaban-common/src/test/java/azkaban/utils/DirectoryFlowLoaderTest.java
new file mode 100644
index 0000000..f658bb5
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/DirectoryFlowLoaderTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014 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.utils;
+
+import java.io.File;
+
+import org.apache.log4j.Logger;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class DirectoryFlowLoaderTest {
+
+ @Ignore @Test
+ public void testDirectoryLoad() {
+ Logger logger = Logger.getLogger(this.getClass());
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+
+ loader.loadProjectFlow(new File("unit/executions/exectest1"));
+ logger.info(loader.getFlowMap().size());
+ }
+
+ @Ignore @Test
+ public void testLoadEmbeddedFlow() {
+ Logger logger = Logger.getLogger(this.getClass());
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+
+ loader.loadProjectFlow(new File("unit/executions/embedded"));
+ Assert.assertEquals(0, loader.getErrors().size());
+ }
+
+ @Ignore @Test
+ public void testRecursiveLoadEmbeddedFlow() {
+ Logger logger = Logger.getLogger(this.getClass());
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+
+ loader.loadProjectFlow(new File("unit/executions/embeddedBad"));
+ for (String error : loader.getErrors()) {
+ System.out.println(error);
+ }
+
+ // Should be 3 errors: jobe->innerFlow, innerFlow->jobe, innerFlow
+ Assert.assertEquals(3, loader.getErrors().size());
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/utils/EmailMessageTest.java b/azkaban-common/src/test/java/azkaban/utils/EmailMessageTest.java
new file mode 100644
index 0000000..db5f00d
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/EmailMessageTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014 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.utils;
+
+import java.io.IOException;
+
+import javax.mail.MessagingException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class EmailMessageTest {
+
+ String host = "";
+ String sender = "";
+ String user = "";
+ String password = "";
+
+ String toAddr = "";
+
+ private EmailMessage em;
+
+ @Before
+ public void setUp() throws Exception {
+ em = new EmailMessage(host, user, password);
+ em.setFromAddress(sender);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Ignore
+ @Test
+ public void testSendEmail() throws IOException {
+ em.addToAddress(toAddr);
+ // em.addToAddress("cyu@linkedin.com");
+ em.setSubject("azkaban test email");
+ em.setBody("azkaban test email");
+ try {
+ em.sendEmail();
+ } catch (MessagingException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/utils/FileIOUtilsTest.java b/azkaban-common/src/test/java/azkaban/utils/FileIOUtilsTest.java
new file mode 100644
index 0000000..99c6464
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/FileIOUtilsTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2014 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.utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+import org.apache.commons.io.FileUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class FileIOUtilsTest {
+ File sourceDir = new File("unit/project/testjob");
+ File destDir = new File("unit/executions/unixsymlink");
+
+ @Before
+ public void setUp() throws Exception {
+ if (destDir.exists()) {
+ FileUtils.deleteDirectory(destDir);
+ }
+ destDir.mkdirs();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Ignore @Test
+ public void testSymlinkCopy() throws IOException {
+ FileIOUtils.createDeepSymlink(sourceDir, destDir);
+ }
+
+ @Test
+ public void testSymlinkCopyNonSource() {
+ boolean exception = false;
+ try {
+ FileIOUtils.createDeepSymlink(new File(sourceDir, "idonotexist"), destDir);
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ System.out.println("Handled this case nicely.");
+ exception = true;
+ }
+
+ Assert.assertTrue(exception);
+ }
+
+ @Test
+ public void testAsciiUTF() throws IOException {
+ String foreignText = "abcdefghijklmnopqrstuvwxyz";
+ byte[] utf8ByteArray = createUTF8ByteArray(foreignText);
+
+ int length = utf8ByteArray.length;
+ System.out.println("char length:" + foreignText.length() +
+ " utf8BytesLength:" + utf8ByteArray.length + " for:" + foreignText);
+
+ Pair<Integer,Integer> pair = FileIOUtils.getUtf8Range(utf8ByteArray, 1,
+ length - 6);
+ System.out.println("Pair :" + pair.toString());
+
+ String recreatedString = new String(utf8ByteArray, 1, length - 6, "UTF-8");
+ System.out.println("recreatedString:" + recreatedString);
+
+ String correctString = new String(utf8ByteArray, pair.getFirst(),
+ pair.getSecond(), "UTF-8");
+ System.out.println("correctString:" + correctString);
+
+ Assert.assertEquals(pair, new Pair<Integer,Integer>(1, 20));
+ // Two characters stripped from this.
+ Assert.assertEquals(correctString.length(), foreignText.length() - 6);
+
+ }
+
+ @Test
+ public void testForeignUTF() throws IOException {
+ String foreignText = "안녕하세요, 제 이름은 박병호입니다";
+ byte[] utf8ByteArray = createUTF8ByteArray(foreignText);
+
+ int length = utf8ByteArray.length;
+ System.out.println("char length:" + foreignText.length()
+ + " utf8BytesLength:" + utf8ByteArray.length + " for:" + foreignText);
+
+ Pair<Integer,Integer> pair = FileIOUtils.getUtf8Range(utf8ByteArray, 1,
+ length - 6);
+ System.out.println("Pair :" + pair.toString());
+
+ String recreatedString = new String(utf8ByteArray, 1, length - 6, "UTF-8");
+ System.out.println("recreatedString:" + recreatedString);
+
+ String correctString = new String(utf8ByteArray, pair.getFirst(),
+ pair.getSecond(), "UTF-8");
+ System.out.println("correctString:" + correctString);
+
+ Assert.assertEquals(pair, new Pair<Integer,Integer>(3, 40));
+ // Two characters stripped from this.
+ Assert.assertEquals(correctString.length(), foreignText.length() - 3);
+
+
+ // Testing mixed bytes
+ String mixedText = "abc안녕하세요, 제 이름은 박병호입니다";
+ byte[] mixedBytes = createUTF8ByteArray(mixedText);
+ Pair<Integer,Integer> pair2 = FileIOUtils.getUtf8Range(mixedBytes, 1,
+ length - 4);
+ correctString = new String(mixedBytes, pair2.getFirst(), pair2.getSecond(),
+ "UTF-8");
+ System.out.println("correctString:" + correctString);
+ Assert.assertEquals(pair2, new Pair<Integer,Integer>(1, 45));
+ // Two characters stripped from this.
+ Assert.assertEquals(correctString.length(), mixedText.length() - 3);
+
+ }
+
+ private byte[] createUTF8ByteArray(String text) {
+ byte[] textBytes= null;
+ try {
+ textBytes = text.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ return textBytes;
+ }
+}
diff --git a/azkaban-common/src/test/java/azkaban/utils/JsonUtilsTest.java b/azkaban-common/src/test/java/azkaban/utils/JsonUtilsTest.java
new file mode 100644
index 0000000..c48946b
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/JsonUtilsTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2014 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.utils;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JsonUtilsTest {
+ @Test
+ public void writePropsNoJarDependencyTest1() throws IOException {
+ Map<String, String> test = new HashMap<String, String>();
+ test.put("\"myTest\n\b", "myValue\t\\");
+ test.put("normalKey", "Other key");
+
+ StringWriter writer = new StringWriter();
+ JSONUtils.writePropsNoJarDependency(test, writer);
+
+ String jsonStr = writer.toString();
+ System.out.println(writer.toString());
+
+ @SuppressWarnings("unchecked")
+ Map<String, String> result =
+ (Map<String, String>) JSONUtils.parseJSONFromString(jsonStr);
+ checkInAndOut(test, result);
+ }
+
+ @Test
+ public void writePropsNoJarDependencyTest2() throws IOException {
+ Map<String, String> test = new HashMap<String, String>();
+ test.put("\"myTest\n\b", "myValue\t\\");
+
+ StringWriter writer = new StringWriter();
+ JSONUtils.writePropsNoJarDependency(test, writer);
+
+ String jsonStr = writer.toString();
+ System.out.println(writer.toString());
+
+ @SuppressWarnings("unchecked")
+ Map<String, String> result =
+ (Map<String, String>) JSONUtils.parseJSONFromString(jsonStr);
+ checkInAndOut(test, result);
+ }
+
+ private static void checkInAndOut(Map<String, String> before,
+ Map<String, String> after) {
+ for (Map.Entry<String, String> entry : before.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+
+ String retValue = after.get(key);
+ Assert.assertEquals(value, retValue);
+ }
+ }
+
+}
diff --git a/azkaban-common/src/test/java/azkaban/utils/PropsUtilsTest.java b/azkaban-common/src/test/java/azkaban/utils/PropsUtilsTest.java
new file mode 100644
index 0000000..158558e
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/PropsUtilsTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2014 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.utils;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PropsUtilsTest {
+ @Test
+ public void testGoodResolveProps() throws IOException {
+ Props propsGrandParent = new Props();
+ Props propsParent = new Props(propsGrandParent);
+ Props props = new Props(propsParent);
+
+ // Testing props in general
+ props.put("letter", "a");
+ propsParent.put("letter", "b");
+ propsGrandParent.put("letter", "c");
+
+ Assert.assertEquals("a", props.get("letter"));
+ propsParent.put("my", "name");
+ propsParent.put("your", "eyes");
+ propsGrandParent.put("their", "ears");
+ propsGrandParent.put("your", "hair");
+
+ Assert.assertEquals("name", props.get("my"));
+ Assert.assertEquals("eyes", props.get("your"));
+ Assert.assertEquals("ears", props.get("their"));
+
+ props.put("res1", "${my}");
+ props.put("res2", "${their} ${letter}");
+ props.put("res7", "${my} ${res5}");
+
+ propsParent.put("res3", "${your} ${their} ${res4}");
+ propsGrandParent.put("res4", "${letter}");
+ propsGrandParent.put("res5", "${their}");
+ propsParent.put("res6", " t ${your} ${your} ${their} ${res5}");
+
+ Props resolved = PropsUtils.resolveProps(props);
+ Assert.assertEquals("name", resolved.get("res1"));
+ Assert.assertEquals("ears a", resolved.get("res2"));
+ Assert.assertEquals("eyes ears a", resolved.get("res3"));
+ Assert.assertEquals("a", resolved.get("res4"));
+ Assert.assertEquals("ears", resolved.get("res5"));
+ Assert.assertEquals(" t eyes eyes ears ears", resolved.get("res6"));
+ Assert.assertEquals("name ears", resolved.get("res7"));
+ }
+
+ @Test
+ public void testInvalidSyntax() throws Exception {
+ Props propsGrandParent = new Props();
+ Props propsParent = new Props(propsGrandParent);
+ Props props = new Props(propsParent);
+
+ propsParent.put("my", "name");
+ props.put("res1", "$(my)");
+
+ Props resolved = PropsUtils.resolveProps(props);
+ Assert.assertEquals("$(my)", resolved.get("res1"));
+ }
+
+ @Test
+ public void testExpressionResolution() throws IOException {
+ Props props =
+ Props.of("normkey", "normal", "num1", "1", "num2", "2", "num3", "3",
+ "variablereplaced", "${num1}", "expression1", "$(1+10)",
+ "expression2", "$(1+10)*2", "expression3",
+ "$($(${num1} + ${num3})*10)", "expression4",
+ "$(${num1} + ${expression3})", "expression5",
+ "$($($(2+3)) + 3) + $(${expression3} + 1)", "expression6",
+ "$(1 + ${normkey})", "expression7", "$(\"${normkey}\" + 1)",
+ "expression8", "${expression1}", "expression9", "$((2+3) + 3)");
+
+ Props resolved = PropsUtils.resolveProps(props);
+ Assert.assertEquals("normal", resolved.get("normkey"));
+ Assert.assertEquals("1", resolved.get("num1"));
+ Assert.assertEquals("2", resolved.get("num2"));
+ Assert.assertEquals("3", resolved.get("num3"));
+ Assert.assertEquals("1", resolved.get("variablereplaced"));
+ Assert.assertEquals("11", resolved.get("expression1"));
+ Assert.assertEquals("11*2", resolved.get("expression2"));
+ Assert.assertEquals("40", resolved.get("expression3"));
+ Assert.assertEquals("41", resolved.get("expression4"));
+ Assert.assertEquals("8 + 41", resolved.get("expression5"));
+ Assert.assertEquals("1", resolved.get("expression6"));
+ Assert.assertEquals("normal1", resolved.get("expression7"));
+ Assert.assertEquals("11", resolved.get("expression8"));
+ Assert.assertEquals("8", resolved.get("expression9"));
+ }
+
+ @Test
+ public void testMalformedExpressionProps() throws IOException {
+ // unclosed
+ Props props = Props.of("key", "$(1+2");
+ failIfNotException(props);
+
+ props = Props.of("key", "$((1+2)");
+ failIfNotException(props);
+
+ // bad variable replacement
+ props = Props.of("key", "$(${dontexist}+2)");
+ failIfNotException(props);
+
+ // bad expression
+ props = Props.of("key", "$(2 +)");
+ failIfNotException(props);
+
+ // bad expression
+ props = Props.of("key", "$(2 + #hello)");
+ failIfNotException(props);
+ }
+
+ @Test
+ public void testCyclesResolveProps() throws IOException {
+ Props propsGrandParent = new Props();
+ Props propsParent = new Props(propsGrandParent);
+ Props props = new Props(propsParent);
+
+ // Testing props in general
+ props.put("a", "${a}");
+ failIfNotException(props);
+
+ props.put("a", "${b}");
+ props.put("b", "${a}");
+ failIfNotException(props);
+
+ props.clearLocal();
+ props.put("a", "${b}");
+ props.put("b", "${c}");
+ propsParent.put("d", "${a}");
+ failIfNotException(props);
+
+ props.clearLocal();
+ props.put("a", "testing ${b}");
+ props.put("b", "${c}");
+ propsGrandParent.put("c", "${d}");
+ propsParent.put("d", "${a}");
+ failIfNotException(props);
+
+ props.clearLocal();
+ props.put("a", "testing ${c} ${b}");
+ props.put("b", "${c} test");
+ propsGrandParent.put("c", "${d}");
+ propsParent.put("d", "${a}");
+ failIfNotException(props);
+ }
+
+ private void failIfNotException(Props props) {
+ try {
+ PropsUtils.resolveProps(props);
+ Assert.fail();
+ } catch (UndefinedPropertyException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/azkaban-common/src/test/resources/conf/dbtesth2/azkaban.properties b/azkaban-common/src/test/resources/conf/dbtesth2/azkaban.properties
new file mode 100644
index 0000000..63ed5a9
--- /dev/null
+++ b/azkaban-common/src/test/resources/conf/dbtesth2/azkaban.properties
@@ -0,0 +1,3 @@
+database.sql.scripts.dir=unit/sql
+database.type=h2
+h2.path=h2dbtest/h2db
diff --git a/azkaban-common/src/test/resources/plugins/jobtypes/anothertestjob/private.properties b/azkaban-common/src/test/resources/plugins/jobtypes/anothertestjob/private.properties
new file mode 100644
index 0000000..8e95c94
--- /dev/null
+++ b/azkaban-common/src/test/resources/plugins/jobtypes/anothertestjob/private.properties
@@ -0,0 +1,2 @@
+jobtype.classpath=lib/*
+jobtype.class=azkaban.test.jobtype.FakeJavaJob
diff --git a/azkaban-common/src/test/resources/plugins/jobtypes/common.properties b/azkaban-common/src/test/resources/plugins/jobtypes/common.properties
new file mode 100644
index 0000000..2823a83
--- /dev/null
+++ b/azkaban-common/src/test/resources/plugins/jobtypes/common.properties
@@ -0,0 +1,3 @@
+commonprop1=commonprop1
+commonprop2=commonprop2
+commonprop3=commonprop3
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/plugins/jobtypes/commonprivate.properties b/azkaban-common/src/test/resources/plugins/jobtypes/commonprivate.properties
new file mode 100644
index 0000000..7d46d43
--- /dev/null
+++ b/azkaban-common/src/test/resources/plugins/jobtypes/commonprivate.properties
@@ -0,0 +1,3 @@
+commonprivate1=commonprivate1
+commonprivate2=commonprivate2
+commonprivate3=commonprivate3
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/plugins/jobtypes/testjob/plugin.properties b/azkaban-common/src/test/resources/plugins/jobtypes/testjob/plugin.properties
new file mode 100644
index 0000000..587081a
--- /dev/null
+++ b/azkaban-common/src/test/resources/plugins/jobtypes/testjob/plugin.properties
@@ -0,0 +1,4 @@
+pluginprops1=1
+pluginprops2=2
+pluginprops3=3
+commonprop3=pluginprops
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/plugins/jobtypes/testjob/private.properties b/azkaban-common/src/test/resources/plugins/jobtypes/testjob/private.properties
new file mode 100644
index 0000000..2bba593
--- /dev/null
+++ b/azkaban-common/src/test/resources/plugins/jobtypes/testjob/private.properties
@@ -0,0 +1,3 @@
+jobtype.class=azkaban.test.jobtype.FakeJavaJob2
+commonprivate3=private3
+testprivate=0
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/project/testfailure/failflow.job b/azkaban-common/src/test/resources/project/testfailure/failflow.job
new file mode 100644
index 0000000..a65b7c4
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testfailure/failflow.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=5
+fail=false
+dependencies=myjob2-fail30,myjob2
diff --git a/azkaban-common/src/test/resources/project/testfailure/myjob1.job b/azkaban-common/src/test/resources/project/testfailure/myjob1.job
new file mode 100644
index 0000000..660e2a9
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testfailure/myjob1.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=10
+fail=false
diff --git a/azkaban-common/src/test/resources/project/testfailure/myjob2.job b/azkaban-common/src/test/resources/project/testfailure/myjob2.job
new file mode 100644
index 0000000..f5a05ed
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testfailure/myjob2.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=30
+fail=false
diff --git a/azkaban-common/src/test/resources/project/testfailure/myjob2-fail30.job b/azkaban-common/src/test/resources/project/testfailure/myjob2-fail30.job
new file mode 100644
index 0000000..03540bb
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testfailure/myjob2-fail30.job
@@ -0,0 +1,6 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=10
+fail=true
+passRetry=2
+dependencies=myjob3
diff --git a/azkaban-common/src/test/resources/project/testfailure/myjob3.job b/azkaban-common/src/test/resources/project/testfailure/myjob3.job
new file mode 100644
index 0000000..a1d7ded
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testfailure/myjob3.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=5
+dependencies=myjob1
+fail=false
diff --git a/azkaban-common/src/test/resources/project/testfailure/test.jar b/azkaban-common/src/test/resources/project/testfailure/test.jar
new file mode 100644
index 0000000..4f1955a
Binary files /dev/null and b/azkaban-common/src/test/resources/project/testfailure/test.jar differ
diff --git a/azkaban-common/src/test/resources/project/testfailure/testfailure.zip b/azkaban-common/src/test/resources/project/testfailure/testfailure.zip
new file mode 100644
index 0000000..dc0b640
Binary files /dev/null and b/azkaban-common/src/test/resources/project/testfailure/testfailure.zip differ
diff --git a/azkaban-common/src/test/resources/project/testflow/flowTestWithError.zip b/azkaban-common/src/test/resources/project/testflow/flowTestWithError.zip
new file mode 100644
index 0000000..e84cfc7
Binary files /dev/null and b/azkaban-common/src/test/resources/project/testflow/flowTestWithError.zip differ
diff --git a/azkaban-common/src/test/resources/project/testflow/testFlow.zip b/azkaban-common/src/test/resources/project/testflow/testFlow.zip
new file mode 100644
index 0000000..d1b44c5
Binary files /dev/null and b/azkaban-common/src/test/resources/project/testflow/testFlow.zip differ
diff --git a/azkaban-common/src/test/resources/project/testjob/expanded/fail-job.job b/azkaban-common/src/test/resources/project/testjob/expanded/fail-job.job
new file mode 100644
index 0000000..fdc1050
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testjob/expanded/fail-job.job
@@ -0,0 +1,2 @@
+type=java
+job.class=azkaban.jobbies.FailJob
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/project/testjob/expanded/push-job.job b/azkaban-common/src/test/resources/project/testjob/expanded/push-job.job
new file mode 100644
index 0000000..5431788
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testjob/expanded/push-job.job
@@ -0,0 +1,5 @@
+type=propertyPusher
+
+prop-dependency=test-job-2
+
+dependencies=fail-job,test-job
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/project/testjob/expanded/test-job.job b/azkaban-common/src/test/resources/project/testjob/expanded/test-job.job
new file mode 100644
index 0000000..a291465
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testjob/expanded/test-job.job
@@ -0,0 +1,2 @@
+type=java
+job.class=azkaban.jobbies.TestJob
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/project/testjob/expanded/test-job-2.job b/azkaban-common/src/test/resources/project/testjob/expanded/test-job-2.job
new file mode 100644
index 0000000..fcea87d
--- /dev/null
+++ b/azkaban-common/src/test/resources/project/testjob/expanded/test-job-2.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.jobbies.TestJob
+
+pass-on.howdy="hi"
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/project/testjob/testjob.zip b/azkaban-common/src/test/resources/project/testjob/testjob.zip
new file mode 100644
index 0000000..5b46b6d
Binary files /dev/null and b/azkaban-common/src/test/resources/project/testjob/testjob.zip differ
diff --git a/azkaban-common/src/test/resources/project/testjob/testjob-med.zip b/azkaban-common/src/test/resources/project/testjob/testjob-med.zip
new file mode 100644
index 0000000..5b46b6d
Binary files /dev/null and b/azkaban-common/src/test/resources/project/testjob/testjob-med.zip differ
diff --git a/azkaban-common/src/test/resources/sql/database.properties b/azkaban-common/src/test/resources/sql/database.properties
new file mode 100644
index 0000000..7b323f0
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/database.properties
@@ -0,0 +1 @@
+version=2.2
diff --git a/azkaban-common/src/test/resources/sql/update.execution_jobs.2.1.sql b/azkaban-common/src/test/resources/sql/update.execution_jobs.2.1.sql
new file mode 100644
index 0000000..b7313ad
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/update.execution_jobs.2.1.sql
@@ -0,0 +1,4 @@
+ALTER TABLE execution_jobs ADD COLUMN attempt INT DEFAULT 0;
+ALTER TABLE execution_jobs DROP PRIMARY KEY;
+ALTER TABLE execution_jobs ADD PRIMARY KEY(exec_id, job_id, attempt);
+ALTER TABLE execution_jobs ADD INDEX exec_job (exec_id, job_id);
diff --git a/azkaban-common/src/test/resources/sql/update.execution_jobs.2.3.sql b/azkaban-common/src/test/resources/sql/update.execution_jobs.2.3.sql
new file mode 100644
index 0000000..8c43495
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/update.execution_jobs.2.3.sql
@@ -0,0 +1 @@
+INSERT INTO properties (name,value,modified_time,type) VALUES ('test4', 'value1', 0, 99);
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/sql/update.execution_logs.2.1.sql b/azkaban-common/src/test/resources/sql/update.execution_logs.2.1.sql
new file mode 100644
index 0000000..5c2dc0b
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/update.execution_logs.2.1.sql
@@ -0,0 +1,7 @@
+ALTER TABLE execution_logs ADD COLUMN attempt INT DEFAULT 0;
+ALTER TABLE execution_logs ADD COLUMN upload_time BIGINT DEFAULT 1420099200000;
+UPDATE execution_logs SET upload_time=(UNIX_TIMESTAMP()*1000) WHERE upload_time=1420099200000;
+
+ALTER TABLE execution_logs DROP PRIMARY KEY;
+ALTER TABLE execution_logs ADD PRIMARY KEY(exec_id, name, attempt, start_byte);
+ALTER TABLE execution_logs ADD INDEX ex_log_attempt (exec_id, name, attempt)
diff --git a/azkaban-common/src/test/resources/sql/update.execution_logs.2.4.sql b/azkaban-common/src/test/resources/sql/update.execution_logs.2.4.sql
new file mode 100644
index 0000000..f0a7aae
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/update.execution_logs.2.4.sql
@@ -0,0 +1 @@
+INSERT INTO properties (name,value,modified_time,type) VALUES ('test', 'value1', 0, 99);
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/sql/update.execution_logs.2.7.sql b/azkaban-common/src/test/resources/sql/update.execution_logs.2.7.sql
new file mode 100644
index 0000000..9974cc7
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/update.execution_logs.2.7.sql
@@ -0,0 +1 @@
+INSERT INTO properties (name,value,modified_time,type) VALUES ('test1','value1', 0, 99);
\ No newline at end of file
diff --git a/azkaban-common/src/test/resources/sql/update.project_events.2.1.sql b/azkaban-common/src/test/resources/sql/update.project_events.2.1.sql
new file mode 100644
index 0000000..14d7554
--- /dev/null
+++ b/azkaban-common/src/test/resources/sql/update.project_events.2.1.sql
@@ -0,0 +1 @@
+ALTER TABLE project_events MODIFY COLUMN message VARCHAR(512);
diff --git a/azkaban-common/src/test/resources/test-conf/azkaban-users-test1.xml b/azkaban-common/src/test/resources/test-conf/azkaban-users-test1.xml
new file mode 100644
index 0000000..4c44882
--- /dev/null
+++ b/azkaban-common/src/test/resources/test-conf/azkaban-users-test1.xml
@@ -0,0 +1,12 @@
+<azkaban-users>
+ <user username="user0" password="password0" roles="role0" groups="group0"/>
+ <user username="user1" password="password1" roles="role0,role1" groups="group1,group2"/>
+ <user username="user2" password="password2" roles="role0,role1,role2" groups="group1,group2,group3"/>
+ <user username="user3" password="password3" roles="role1, role2" groups="group1, group2"/>
+ <user username="user4" password="password4" roles="role1 , role2" groups="group1 , group2"/>
+ <user username="user5" password="password5" roles="role1 , role2," groups="group1 , group2,"/>
+ <user username="user6" password="password6" roles="role3 , role2, " groups="group1 , group2, "/>
+ <user username="user7" password="password7" groups="group1"/>
+ <user username="user8" password="password8" roles="role3"/>
+ <user username="user9" password="password9"/>
+</azkaban-users>
\ No newline at end of file
azkaban-execserver/.gitignore 2(+2 -0)
diff --git a/azkaban-execserver/.gitignore b/azkaban-execserver/.gitignore
new file mode 100644
index 0000000..4a18069
--- /dev/null
+++ b/azkaban-execserver/.gitignore
@@ -0,0 +1,2 @@
+_AzkabanTestDir*
+*.log
diff --git a/azkaban-execserver/src/main/resources/log4j.properties b/azkaban-execserver/src/main/resources/log4j.properties
new file mode 100644
index 0000000..e304ac7
--- /dev/null
+++ b/azkaban-execserver/src/main/resources/log4j.properties
@@ -0,0 +1,13 @@
+log4j.rootLogger=INFO, Console
+log4j.logger.azkaban.execapp=INFO, ExecServer
+
+log4j.appender.ExecServer=org.apache.log4j.RollingFileAppender
+log4j.appender.ExecServer.layout=org.apache.log4j.PatternLayout
+log4j.appender.ExecServer.File=azkaban-execserver.log
+log4j.appender.ExecServer.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
+log4j.appender.ExecServer.MaxFileSize=102400MB
+log4j.appender.ExecServer.MaxBackupIndex=2
+
+log4j.appender.Console=org.apache.log4j.ConsoleAppender
+log4j.appender.Console.layout=org.apache.log4j.PatternLayout
+log4j.appender.Console.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
diff --git a/azkaban-execserver/src/package/bin/azkaban-executor-start.sh b/azkaban-execserver/src/package/bin/azkaban-executor-start.sh
new file mode 100755
index 0000000..c281890
--- /dev/null
+++ b/azkaban-execserver/src/package/bin/azkaban-executor-start.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+azkaban_dir=$(dirname $0)/..
+
+if [[ -z "$tmpdir" ]]; then
+tmpdir=/tmp
+fi
+
+for file in $azkaban_dir/lib/*.jar;
+do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+for file in $azkaban_dir/extlib/*.jar;
+do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+for file in $azkaban_dir/plugins/*/*.jar;
+do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+if [ "$HADOOP_HOME" != "" ]; then
+ echo "Using Hadoop from $HADOOP_HOME"
+ CLASSPATH=$CLASSPATH:$HADOOP_HOME/conf:$HADOOP_HOME/*
+ JAVA_LIB_PATH="-Djava.library.path=$HADOOP_HOME/lib/native/Linux-amd64-64"
+else
+ echo "Error: HADOOP_HOME is not set. Hadoop job types will not run properly."
+fi
+
+if [ "$HIVE_HOME" != "" ]; then
+ echo "Using Hive from $HIVE_HOME"
+ CLASSPATH=$CLASSPATH:$HIVE_HOME/conf:$HIVE_HOME/lib/*
+fi
+
+echo $azkaban_dir;
+echo $CLASSPATH;
+
+executorport=`cat $azkaban_dir/conf/azkaban.properties | grep executor.port | cut -d = -f 2`
+echo "Starting AzkabanExecutorServer on port $executorport ..."
+serverpath=`pwd`
+
+if [ -z $AZKABAN_OPTS ]; then
+ AZKABAN_OPTS="-Xmx3G"
+fi
+AZKABAN_OPTS="$AZKABAN_OPTS -server -Dcom.sun.management.jmxremote -Djava.io.tmpdir=$tmpdir -Dexecutorport=$executorport -Dserverpath=$serverpath"
+
+java $AZKABAN_OPTS $JAVA_LIB_PATH -cp $CLASSPATH azkaban.execapp.AzkabanExecutorServer -conf $azkaban_dir/conf $@ &
+
+echo $! > $azkaban_dir/currentpid
+
diff --git a/azkaban-execserver/src/package/conf/azkaban.private.properties b/azkaban-execserver/src/package/conf/azkaban.private.properties
new file mode 100644
index 0000000..cce1792
--- /dev/null
+++ b/azkaban-execserver/src/package/conf/azkaban.private.properties
@@ -0,0 +1 @@
+# Optional Properties that are hidden to the executions
\ No newline at end of file
diff --git a/azkaban-execserver/src/package/conf/global.properties b/azkaban-execserver/src/package/conf/global.properties
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/azkaban-execserver/src/package/conf/global.properties
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
new file mode 100644
index 0000000..87c668d
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2014 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.execapp.event;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import azkaban.executor.Status;
+
+public class BlockingStatusTest {
+
+ public class WatchingThread extends Thread {
+ private BlockingStatus status;
+ private long diff = 0;
+
+ public WatchingThread(BlockingStatus status) {
+ this.status = status;
+ }
+
+ public void run() {
+ long startTime = System.currentTimeMillis();
+ status.blockOnFinishedStatus();
+ diff = System.currentTimeMillis() - startTime;
+ }
+
+ public long getDiff() {
+ return diff;
+ }
+ }
+
+ @Test
+ public void testFinishedBlock() {
+ BlockingStatus status = new BlockingStatus(1, "test", Status.SKIPPED);
+
+ WatchingThread thread = new WatchingThread(status);
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("Diff " + thread.getDiff());
+ Assert.assertTrue(thread.getDiff() < 100);
+ }
+
+ @Test
+ public void testUnfinishedBlock() throws InterruptedException {
+ BlockingStatus status = new BlockingStatus(1, "test", Status.QUEUED);
+
+ WatchingThread thread = new WatchingThread(status);
+ thread.start();
+
+ synchronized (this) {
+ wait(3000);
+ }
+
+ status.changeStatus(Status.SUCCEEDED);
+ thread.join();
+
+ System.out.println("Diff " + thread.getDiff());
+ Assert.assertTrue(thread.getDiff() >= 3000 && thread.getDiff() < 3100);
+ }
+
+ @Test
+ public void testUnfinishedBlockSeveralChanges() throws InterruptedException {
+ BlockingStatus status = new BlockingStatus(1, "test", Status.QUEUED);
+
+ WatchingThread thread = new WatchingThread(status);
+ thread.start();
+
+ synchronized (this) {
+ wait(3000);
+ }
+
+ status.changeStatus(Status.PAUSED);
+
+ synchronized (this) {
+ wait(1000);
+ }
+
+ status.changeStatus(Status.FAILED);
+
+ thread.join(1000);
+
+ System.out.println("Diff " + thread.getDiff());
+ Assert.assertTrue(thread.getDiff() >= 4000 && thread.getDiff() < 4100);
+ }
+
+ @Test
+ public void testMultipleWatchers() throws InterruptedException {
+ BlockingStatus status = new BlockingStatus(1, "test", Status.QUEUED);
+
+ WatchingThread thread1 = new WatchingThread(status);
+ thread1.start();
+
+ synchronized (this) {
+ wait(2000);
+ }
+
+ WatchingThread thread2 = new WatchingThread(status);
+ thread2.start();
+
+ synchronized (this) {
+ wait(2000);
+ }
+
+ status.changeStatus(Status.FAILED);
+ thread2.join(1000);
+ thread1.join(1000);
+
+ System.out.println("Diff thread 1 " + thread1.getDiff());
+ System.out.println("Diff thread 2 " + thread2.getDiff());
+ Assert.assertTrue(thread1.getDiff() >= 4000 && thread1.getDiff() < 4100);
+ Assert.assertTrue(thread2.getDiff() >= 2000 && thread2.getDiff() < 2100);
+ }
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/event/LocalFlowWatcherTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/event/LocalFlowWatcherTest.java
new file mode 100644
index 0000000..a75f9b6
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/event/LocalFlowWatcherTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2014 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.execapp.event;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.apache.commons.io.FileUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.execapp.EventCollectorListener;
+import azkaban.execapp.FlowRunner;
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutionOptions;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.executor.Status;
+import azkaban.flow.Flow;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.project.Project;
+import azkaban.project.ProjectLoader;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.JSONUtils;
+
+public class LocalFlowWatcherTest {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private ProjectLoader fakeProjectLoader;
+ private int dirVal = 0;
+
+ @Before
+ public void setUp() throws Exception {
+ jobtypeManager =
+ new JobTypeManager(null, null, this.getClass().getClassLoader());
+ jobtypeManager.getJobTypePluginSet().addPluginClass("java", JavaJob.class);
+ fakeProjectLoader = new MockProjectLoader(workingDir);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ }
+
+ public File setupDirectory() throws IOException {
+ System.out.println("Create temp dir");
+ File workingDir = new File("_AzkabanTestDir_" + dirVal);
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ dirVal++;
+
+ return workingDir;
+ }
+
+ @Ignore @Test
+ public void testBasicLocalFlowWatcher() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+
+ File workingDir1 = setupDirectory();
+ FlowRunner runner1 =
+ createFlowRunner(workingDir1, loader, eventCollector, "exec1", 1, null,
+ null);
+ Thread runner1Thread = new Thread(runner1);
+
+ File workingDir2 = setupDirectory();
+ LocalFlowWatcher watcher = new LocalFlowWatcher(runner1);
+ FlowRunner runner2 =
+ createFlowRunner(workingDir2, loader, eventCollector, "exec1", 2,
+ watcher, 2);
+ Thread runner2Thread = new Thread(runner2);
+
+ runner1Thread.start();
+ runner2Thread.start();
+ runner2Thread.join();
+
+ FileUtils.deleteDirectory(workingDir1);
+ FileUtils.deleteDirectory(workingDir2);
+
+ testPipelineLevel2(runner1.getExecutableFlow(), runner2.getExecutableFlow());
+ }
+
+ @Ignore @Test
+ public void testLevel1LocalFlowWatcher() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+
+ File workingDir1 = setupDirectory();
+ FlowRunner runner1 =
+ createFlowRunner(workingDir1, loader, eventCollector, "exec1", 1, null,
+ null);
+ Thread runner1Thread = new Thread(runner1);
+
+ File workingDir2 = setupDirectory();
+ LocalFlowWatcher watcher = new LocalFlowWatcher(runner1);
+ FlowRunner runner2 =
+ createFlowRunner(workingDir2, loader, eventCollector, "exec1", 2,
+ watcher, 1);
+ Thread runner2Thread = new Thread(runner2);
+
+ runner1Thread.start();
+ runner2Thread.start();
+ runner2Thread.join();
+
+ FileUtils.deleteDirectory(workingDir1);
+ FileUtils.deleteDirectory(workingDir2);
+
+ testPipelineLevel1(runner1.getExecutableFlow(), runner2.getExecutableFlow());
+ }
+
+ @Ignore @Test
+ public void testLevel2DiffLocalFlowWatcher() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+
+ File workingDir1 = setupDirectory();
+ FlowRunner runner1 =
+ createFlowRunner(workingDir1, loader, eventCollector, "exec1", 1, null,
+ null);
+ Thread runner1Thread = new Thread(runner1);
+
+ File workingDir2 = setupDirectory();
+ LocalFlowWatcher watcher = new LocalFlowWatcher(runner1);
+ FlowRunner runner2 =
+ createFlowRunner(workingDir2, loader, eventCollector, "exec1-mod", 2,
+ watcher, 1);
+ Thread runner2Thread = new Thread(runner2);
+
+ runner1Thread.start();
+ runner2Thread.start();
+ runner2Thread.join();
+
+ FileUtils.deleteDirectory(workingDir1);
+ FileUtils.deleteDirectory(workingDir2);
+
+ testPipelineLevel1(runner1.getExecutableFlow(), runner2.getExecutableFlow());
+ }
+
+ private void testPipelineLevel1(ExecutableFlow first, ExecutableFlow second) {
+ for (ExecutableNode node : second.getExecutableNodes()) {
+ Assert.assertEquals(node.getStatus(), Status.SUCCEEDED);
+
+ // check it's start time is after the first's children.
+ ExecutableNode watchedNode = first.getExecutableNode(node.getId());
+ if (watchedNode == null) {
+ continue;
+ }
+ Assert.assertEquals(watchedNode.getStatus(), Status.SUCCEEDED);
+
+ System.out.println("Node " + node.getId() + " start: "
+ + node.getStartTime() + " dependent on " + watchedNode.getId() + " "
+ + watchedNode.getEndTime() + " diff: "
+ + (node.getStartTime() - watchedNode.getEndTime()));
+
+ Assert.assertTrue(node.getStartTime() >= watchedNode.getEndTime());
+
+ long minParentDiff = 0;
+ if (node.getInNodes().size() > 0) {
+ minParentDiff = Long.MAX_VALUE;
+ for (String dependency : node.getInNodes()) {
+ ExecutableNode parent = second.getExecutableNode(dependency);
+ long diff = node.getStartTime() - parent.getEndTime();
+ minParentDiff = Math.min(minParentDiff, diff);
+ }
+ }
+ long diff = node.getStartTime() - watchedNode.getEndTime();
+ System.out.println(" minPipelineTimeDiff:" + diff
+ + " minDependencyTimeDiff:" + minParentDiff);
+ Assert.assertTrue(minParentDiff < 100 || diff < 100);
+ }
+ }
+
+ private void testPipelineLevel2(ExecutableFlow first, ExecutableFlow second) {
+ for (ExecutableNode node : second.getExecutableNodes()) {
+ Assert.assertEquals(node.getStatus(), Status.SUCCEEDED);
+
+ // check it's start time is after the first's children.
+ ExecutableNode watchedNode = first.getExecutableNode(node.getId());
+ if (watchedNode == null) {
+ continue;
+ }
+ Assert.assertEquals(watchedNode.getStatus(), Status.SUCCEEDED);
+
+ long minDiff = Long.MAX_VALUE;
+ for (String watchedChild : watchedNode.getOutNodes()) {
+ ExecutableNode child = first.getExecutableNode(watchedChild);
+ if (child == null) {
+ continue;
+ }
+ Assert.assertEquals(child.getStatus(), Status.SUCCEEDED);
+ long diff = node.getStartTime() - child.getEndTime();
+ minDiff = Math.min(minDiff, diff);
+ System.out.println("Node " + node.getId() + " start: "
+ + node.getStartTime() + " dependent on " + watchedChild + " "
+ + child.getEndTime() + " diff: " + diff);
+
+ Assert.assertTrue(node.getStartTime() >= child.getEndTime());
+ }
+
+ long minParentDiff = Long.MAX_VALUE;
+ for (String dependency : node.getInNodes()) {
+ ExecutableNode parent = second.getExecutableNode(dependency);
+ long diff = node.getStartTime() - parent.getEndTime();
+ minParentDiff = Math.min(minParentDiff, diff);
+ }
+ System.out.println(" minPipelineTimeDiff:" + minDiff
+ + " minDependencyTimeDiff:" + minParentDiff);
+ Assert.assertTrue(minParentDiff < 100 || minDiff < 100);
+ }
+ }
+
+ private FlowRunner createFlowRunner(File workingDir, ExecutorLoader loader,
+ EventCollectorListener eventCollector, String flowName, int execId,
+ FlowWatcher watcher, Integer pipeline) throws Exception {
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow exFlow =
+ prepareExecDir(workingDir, testDir, flowName, execId);
+ ExecutionOptions option = exFlow.getExecutionOptions();
+ if (watcher != null) {
+ option.setPipelineLevel(pipeline);
+ option.setPipelineExecutionId(watcher.getExecId());
+ }
+ // MockProjectLoader projectLoader = new MockProjectLoader(new
+ // File(exFlow.getExecutionPath()));
+
+ loader.uploadExecutableFlow(exFlow);
+ FlowRunner runner =
+ new FlowRunner(exFlow, loader, fakeProjectLoader, jobtypeManager);
+ runner.setFlowWatcher(watcher);
+ runner.addListener(eventCollector);
+
+ return runner;
+ }
+
+ private ExecutableFlow prepareExecDir(File workingDir, File execDir,
+ String flowName, int execId) throws IOException {
+ FileUtils.copyDirectory(execDir, workingDir);
+
+ File jsonFlowFile = new File(workingDir, flowName + ".flow");
+ @SuppressWarnings("unchecked")
+ HashMap<String, Object> flowObj =
+ (HashMap<String, Object>) JSONUtils.parseJSONFromFile(jsonFlowFile);
+
+ Project project = new Project(1, "test");
+ Flow flow = Flow.flowFromObject(flowObj);
+ ExecutableFlow execFlow = new ExecutableFlow(project, flow);
+ execFlow.setExecutionId(execId);
+ execFlow.setExecutionPath(workingDir.getPath());
+ return execFlow;
+ }
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/event/RemoteFlowWatcherTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/event/RemoteFlowWatcherTest.java
new file mode 100644
index 0000000..4cbc794
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/event/RemoteFlowWatcherTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2014 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.execapp.event;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.apache.commons.io.FileUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.execapp.EventCollectorListener;
+import azkaban.execapp.FlowRunner;
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableFlowBase;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutionOptions;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.executor.Status;
+import azkaban.flow.Flow;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.project.Project;
+import azkaban.project.ProjectLoader;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.JSONUtils;
+
+public class RemoteFlowWatcherTest {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private ProjectLoader fakeProjectLoader;
+ private int dirVal = 0;
+
+ @Before
+ public void setUp() throws Exception {
+ jobtypeManager =
+ new JobTypeManager(null, null, this.getClass().getClassLoader());
+ jobtypeManager.getJobTypePluginSet().addPluginClass("java", JavaJob.class);
+ fakeProjectLoader = new MockProjectLoader(workingDir);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ }
+
+ public File setupDirectory() throws IOException {
+ System.out.println("Create temp dir");
+ File workingDir = new File("_AzkabanTestDir_" + dirVal);
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ dirVal++;
+
+ return workingDir;
+ }
+
+ @Ignore @Test
+ public void testBasicRemoteFlowWatcher() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+
+ File workingDir1 = setupDirectory();
+ FlowRunner runner1 =
+ createFlowRunner(workingDir1, loader, eventCollector, "exec1", 1, null,
+ null);
+ Thread runner1Thread = new Thread(runner1);
+
+ File workingDir2 = setupDirectory();
+ RemoteFlowWatcher watcher = new RemoteFlowWatcher(1, loader, 100);
+ FlowRunner runner2 =
+ createFlowRunner(workingDir2, loader, eventCollector, "exec1", 2,
+ watcher, 2);
+ Thread runner2Thread = new Thread(runner2);
+
+ printCurrentState("runner1 ", runner1.getExecutableFlow());
+ runner1Thread.start();
+ runner2Thread.start();
+
+ runner2Thread.join();
+
+ FileUtils.deleteDirectory(workingDir1);
+ FileUtils.deleteDirectory(workingDir2);
+
+ testPipelineLevel2(runner1.getExecutableFlow(), runner2.getExecutableFlow());
+ }
+
+ @Ignore @Test
+ public void testLevel1RemoteFlowWatcher() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+
+ File workingDir1 = setupDirectory();
+ FlowRunner runner1 =
+ createFlowRunner(workingDir1, loader, eventCollector, "exec1", 1, null,
+ null);
+ Thread runner1Thread = new Thread(runner1);
+
+ File workingDir2 = setupDirectory();
+ RemoteFlowWatcher watcher = new RemoteFlowWatcher(1, loader, 100);
+ FlowRunner runner2 =
+ createFlowRunner(workingDir2, loader, eventCollector, "exec1", 2,
+ watcher, 1);
+ Thread runner2Thread = new Thread(runner2);
+
+ runner1Thread.start();
+ runner2Thread.start();
+ runner2Thread.join();
+
+ FileUtils.deleteDirectory(workingDir1);
+ FileUtils.deleteDirectory(workingDir2);
+
+ testPipelineLevel1(runner1.getExecutableFlow(), runner2.getExecutableFlow());
+ }
+
+ @Ignore @Test
+ public void testLevel2DiffRemoteFlowWatcher() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+
+ File workingDir1 = setupDirectory();
+ FlowRunner runner1 =
+ createFlowRunner(workingDir1, loader, eventCollector, "exec1", 1, null,
+ null);
+ Thread runner1Thread = new Thread(runner1);
+
+ File workingDir2 = setupDirectory();
+
+ RemoteFlowWatcher watcher = new RemoteFlowWatcher(1, loader, 100);
+ FlowRunner runner2 =
+ createFlowRunner(workingDir2, loader, eventCollector, "exec1-mod", 2,
+ watcher, 1);
+ Thread runner2Thread = new Thread(runner2);
+
+ runner1Thread.start();
+ runner2Thread.start();
+ runner2Thread.join();
+
+ FileUtils.deleteDirectory(workingDir1);
+ FileUtils.deleteDirectory(workingDir2);
+
+ testPipelineLevel1(runner1.getExecutableFlow(), runner2.getExecutableFlow());
+ }
+
+ private void testPipelineLevel1(ExecutableFlow first, ExecutableFlow second) {
+ for (ExecutableNode node : second.getExecutableNodes()) {
+ Assert.assertEquals(node.getStatus(), Status.SUCCEEDED);
+
+ // check it's start time is after the first's children.
+ ExecutableNode watchedNode = first.getExecutableNode(node.getId());
+ if (watchedNode == null) {
+ continue;
+ }
+ Assert.assertEquals(watchedNode.getStatus(), Status.SUCCEEDED);
+
+ System.out.println("Node " + node.getId() + " start: "
+ + node.getStartTime() + " dependent on " + watchedNode.getId() + " "
+ + watchedNode.getEndTime() + " diff: "
+ + (node.getStartTime() - watchedNode.getEndTime()));
+
+ Assert.assertTrue(node.getStartTime() >= watchedNode.getEndTime());
+
+ long minParentDiff = 0;
+ if (node.getInNodes().size() > 0) {
+ minParentDiff = Long.MAX_VALUE;
+ for (String dependency : node.getInNodes()) {
+ ExecutableNode parent = second.getExecutableNode(dependency);
+ long diff = node.getStartTime() - parent.getEndTime();
+ minParentDiff = Math.min(minParentDiff, diff);
+ }
+ }
+ long diff = node.getStartTime() - watchedNode.getEndTime();
+ Assert.assertTrue(minParentDiff < 500 || diff < 500);
+ }
+ }
+
+ private void testPipelineLevel2(ExecutableFlow first, ExecutableFlow second) {
+ for (ExecutableNode node : second.getExecutableNodes()) {
+ Assert.assertEquals(node.getStatus(), Status.SUCCEEDED);
+
+ // check it's start time is after the first's children.
+ ExecutableNode watchedNode = first.getExecutableNode(node.getId());
+ if (watchedNode == null) {
+ continue;
+ }
+ Assert.assertEquals(watchedNode.getStatus(), Status.SUCCEEDED);
+
+ long minDiff = Long.MAX_VALUE;
+ for (String watchedChild : watchedNode.getOutNodes()) {
+ ExecutableNode child = first.getExecutableNode(watchedChild);
+ if (child == null) {
+ continue;
+ }
+ Assert.assertEquals(child.getStatus(), Status.SUCCEEDED);
+ long diff = node.getStartTime() - child.getEndTime();
+ minDiff = Math.min(minDiff, diff);
+ System.out.println("Node " + node.getId() + " start: "
+ + node.getStartTime() + " dependent on " + watchedChild + " "
+ + child.getEndTime() + " diff: " + diff);
+ Assert.assertTrue(node.getStartTime() >= child.getEndTime());
+ }
+
+ long minParentDiff = Long.MAX_VALUE;
+ for (String dependency : node.getInNodes()) {
+ ExecutableNode parent = second.getExecutableNode(dependency);
+ long diff = node.getStartTime() - parent.getEndTime();
+ minParentDiff = Math.min(minParentDiff, diff);
+ }
+ System.out.println(" minPipelineTimeDiff:" + minDiff
+ + " minDependencyTimeDiff:" + minParentDiff);
+ Assert.assertTrue(minParentDiff < 500 || minDiff < 500);
+ }
+ }
+
+ private FlowRunner createFlowRunner(File workingDir, ExecutorLoader loader,
+ EventCollectorListener eventCollector, String flowName, int execId,
+ FlowWatcher watcher, Integer pipeline) throws Exception {
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow exFlow =
+ prepareExecDir(workingDir, testDir, flowName, execId);
+ ExecutionOptions options = exFlow.getExecutionOptions();
+ if (watcher != null) {
+ options.setPipelineLevel(pipeline);
+ options.setPipelineExecutionId(watcher.getExecId());
+ }
+ // MockProjectLoader projectLoader = new MockProjectLoader(new
+ // File(exFlow.getExecutionPath()));
+
+ loader.uploadExecutableFlow(exFlow);
+ FlowRunner runner =
+ new FlowRunner(exFlow, loader, fakeProjectLoader, jobtypeManager);
+ runner.setFlowWatcher(watcher);
+ runner.addListener(eventCollector);
+
+ return runner;
+ }
+
+ private void printCurrentState(String prefix, ExecutableFlowBase flow) {
+ for (ExecutableNode node : flow.getExecutableNodes()) {
+
+ System.err.println(prefix + node.getNestedId() + "->"
+ + node.getStatus().name());
+ if (node instanceof ExecutableFlowBase) {
+ printCurrentState(prefix, (ExecutableFlowBase) node);
+ }
+ }
+ }
+
+ private ExecutableFlow prepareExecDir(File workingDir, File execDir,
+ String flowName, int execId) throws IOException {
+ FileUtils.copyDirectory(execDir, workingDir);
+
+ File jsonFlowFile = new File(workingDir, flowName + ".flow");
+ @SuppressWarnings("unchecked")
+ HashMap<String, Object> flowObj =
+ (HashMap<String, Object>) JSONUtils.parseJSONFromFile(jsonFlowFile);
+
+ Project project = new Project(1, "test");
+ Flow flow = Flow.flowFromObject(flowObj);
+ ExecutableFlow execFlow = new ExecutableFlow(project, flow);
+ execFlow.setExecutionId(execId);
+ execFlow.setExecutionPath(workingDir.getPath());
+ return execFlow;
+ }
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/EventCollectorListener.java b/azkaban-execserver/src/test/java/azkaban/execapp/EventCollectorListener.java
new file mode 100644
index 0000000..7a474e2
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/EventCollectorListener.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import azkaban.event.EventListener;
+import azkaban.event.Event;
+import azkaban.event.Event.Type;
+
+public class EventCollectorListener implements EventListener {
+ private ArrayList<Event> eventList = new ArrayList<Event>();
+ private HashSet<Event.Type> filterOutTypes = new HashSet<Event.Type>();
+
+ public void setEventFilterOut(Event.Type... types) {
+ filterOutTypes.addAll(Arrays.asList(types));
+ }
+
+ @Override
+ public void handleEvent(Event event) {
+ if (!filterOutTypes.contains(event.getType())) {
+ eventList.add(event);
+ }
+ }
+
+ public ArrayList<Event> getEventList() {
+ return eventList;
+ }
+
+ public void writeAllEvents() {
+ for (Event event : eventList) {
+ System.out.print(event.getType());
+ System.out.print(",");
+ }
+ }
+
+ public boolean checkOrdering() {
+ long time = 0;
+ for (Event event : eventList) {
+ if (time > event.getTime()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void checkEventExists(Type[] types) {
+ int index = 0;
+ for (Event event : eventList) {
+ if (event.getRunner() == null) {
+ continue;
+ }
+
+ if (index >= types.length) {
+ throw new RuntimeException("More events than expected. Got "
+ + event.getType());
+ }
+ Type type = types[index++];
+
+ if (type != event.getType()) {
+ throw new RuntimeException("Got " + event.getType() + ", expected "
+ + type + " index:" + index);
+ }
+ }
+
+ if (types.length != index) {
+ throw new RuntimeException("Not enough events.");
+ }
+ }
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerPipelineTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerPipelineTest.java
new file mode 100644
index 0000000..8d258ee
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerPipelineTest.java
@@ -0,0 +1,708 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+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.Ignore;
+import org.junit.Test;
+
+import azkaban.execapp.event.FlowWatcher;
+import azkaban.execapp.event.LocalFlowWatcher;
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableFlowBase;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutionOptions;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.InteractiveTestJob;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.executor.Status;
+import azkaban.flow.Flow;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.jobtype.JobTypePluginSet;
+import azkaban.project.Project;
+import azkaban.project.ProjectLoader;
+import azkaban.project.ProjectManagerException;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.DirectoryFlowLoader;
+
+/**
+ * Flows in this test:
+ * joba
+ * jobb
+ * joba1
+ * jobc->joba
+ * jobd->joba
+ * jobe->jobb,jobc,jobd
+ * jobf->jobe,joba1
+ *
+ * jobb = innerFlow
+ * innerJobA
+ * innerJobB->innerJobA
+ * innerJobC->innerJobB
+ * innerFlow->innerJobB,innerJobC
+ *
+ * jobd=innerFlow2
+ * innerFlow2->innerJobA
+ * @author rpark
+ */
+public class FlowRunnerPipelineTest {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private ProjectLoader fakeProjectLoader;
+ private ExecutorLoader fakeExecutorLoader;
+ private Logger logger = Logger.getLogger(FlowRunnerTest2.class);
+ private Project project;
+ private Map<String, Flow> flowMap;
+ private static int id = 101;
+
+ public FlowRunnerPipelineTest() {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ System.out.println("Create temp dir");
+ workingDir = new File("_AzkabanTestDir_" + System.currentTimeMillis());
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ jobtypeManager =
+ new JobTypeManager(null, null, this.getClass().getClassLoader());
+ JobTypePluginSet pluginSet = jobtypeManager.getJobTypePluginSet();
+
+ pluginSet.addPluginClass("java", JavaJob.class);
+ pluginSet.addPluginClass("test", InteractiveTestJob.class);
+ fakeProjectLoader = new MockProjectLoader(workingDir);
+ fakeExecutorLoader = new MockExecutorLoader();
+ project = new Project(1, "testProject");
+
+ File dir = new File("unit/executions/embedded2");
+ prepareProject(dir);
+
+ InteractiveTestJob.clearTestJobs();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ System.out.println("Teardown temp dir");
+ if (workingDir != null) {
+ FileUtils.deleteDirectory(workingDir);
+ workingDir = null;
+ }
+ }
+
+ @Ignore @Test
+ public void testBasicPipelineLevel1Run() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner previousRunner =
+ createFlowRunner(eventCollector, "jobf", "prev");
+
+ ExecutionOptions options = new ExecutionOptions();
+ options.setPipelineExecutionId(previousRunner.getExecutableFlow()
+ .getExecutionId());
+ options.setPipelineLevel(1);
+ FlowWatcher watcher = new LocalFlowWatcher(previousRunner);
+ FlowRunner pipelineRunner =
+ createFlowRunner(eventCollector, "jobf", "pipe", options);
+ pipelineRunner.setFlowWatcher(watcher);
+
+ Map<String, Status> previousExpectedStateMap =
+ new HashMap<String, Status>();
+ Map<String, Status> pipelineExpectedStateMap =
+ new HashMap<String, Status>();
+ Map<String, ExecutableNode> previousNodeMap =
+ new HashMap<String, ExecutableNode>();
+ Map<String, ExecutableNode> pipelineNodeMap =
+ new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow pipelineFlow = pipelineRunner.getExecutableFlow();
+ ExecutableFlow previousFlow = previousRunner.getExecutableFlow();
+ createExpectedStateMap(previousFlow, previousExpectedStateMap,
+ previousNodeMap);
+ createExpectedStateMap(pipelineFlow, pipelineExpectedStateMap,
+ pipelineNodeMap);
+
+ Thread thread1 = runFlowRunnerInThread(previousRunner);
+ pause(250);
+ Thread thread2 = runFlowRunnerInThread(pipelineRunner);
+ pause(500);
+
+ previousExpectedStateMap.put("joba", Status.RUNNING);
+ previousExpectedStateMap.put("joba1", Status.RUNNING);
+ pipelineExpectedStateMap.put("joba", Status.QUEUED);
+ pipelineExpectedStateMap.put("joba1", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:joba").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("joba", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobb", Status.RUNNING);
+ previousExpectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ previousExpectedStateMap.put("jobd", Status.RUNNING);
+ previousExpectedStateMap.put("jobc", Status.RUNNING);
+ previousExpectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ pipelineExpectedStateMap.put("joba", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:jobb:innerJobA").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ previousExpectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:joba").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("joba", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobb", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobd", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobc", Status.QUEUED);
+ pipelineExpectedStateMap.put("jobd:innerJobA", Status.QUEUED);
+ pipelineExpectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:jobd:innerJobA").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobd:innerFlow2", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ // Finish the previous d side
+ InteractiveTestJob.getTestJob("prev:jobd:innerFlow2").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobd", Status.SUCCEEDED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:jobb:innerJobB").succeedJob();
+ InteractiveTestJob.getTestJob("prev:jobb:innerJobC").succeedJob();
+ InteractiveTestJob.getTestJob("prev:jobc").succeedJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("pipe:jobb:innerJobA").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("jobb:innerJobB", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobb:innerFlow", Status.RUNNING);
+ previousExpectedStateMap.put("jobc", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobc", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:jobb:innerFlow").succeedJob();
+ InteractiveTestJob.getTestJob("pipe:jobc").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("jobb:innerFlow", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobb", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobe", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobc", Status.SUCCEEDED);
+
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:jobb:innerJobB").succeedJob();
+ InteractiveTestJob.getTestJob("pipe:jobb:innerJobC").succeedJob();
+ InteractiveTestJob.getTestJob("prev:jobe").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("jobe", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobb:innerJobB", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobb:innerFlow", Status.RUNNING);
+
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:jobd:innerJobA").succeedJob();
+ InteractiveTestJob.getTestJob("pipe:jobb:innerFlow").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("jobb", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobb:innerFlow", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobd:innerFlow2", Status.RUNNING);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:jobd:innerFlow2").succeedJob();
+ InteractiveTestJob.getTestJob("prev:joba1").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobd", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobf", Status.RUNNING);
+ previousExpectedStateMap.put("joba1", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("joba1", Status.RUNNING);
+ pipelineExpectedStateMap.put("jobe", Status.RUNNING);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:jobe").succeedJob();
+ InteractiveTestJob.getTestJob("prev:jobf").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("jobe", Status.SUCCEEDED);
+ previousExpectedStateMap.put("jobf", Status.SUCCEEDED);
+ Assert.assertEquals(Status.SUCCEEDED, previousFlow.getStatus());
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:joba1").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("joba1", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("jobf", Status.RUNNING);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:jobf").succeedJob();
+ pause(250);
+ Assert.assertEquals(Status.SUCCEEDED, pipelineFlow.getStatus());
+ Assert.assertFalse(thread1.isAlive());
+ Assert.assertFalse(thread2.isAlive());
+ }
+
+ @Ignore @Test
+ public void testBasicPipelineLevel2Run() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner previousRunner =
+ createFlowRunner(eventCollector, "pipelineFlow", "prev");
+
+ ExecutionOptions options = new ExecutionOptions();
+ options.setPipelineExecutionId(previousRunner.getExecutableFlow()
+ .getExecutionId());
+ options.setPipelineLevel(2);
+ FlowWatcher watcher = new LocalFlowWatcher(previousRunner);
+ FlowRunner pipelineRunner =
+ createFlowRunner(eventCollector, "pipelineFlow", "pipe", options);
+ pipelineRunner.setFlowWatcher(watcher);
+
+ Map<String, Status> previousExpectedStateMap =
+ new HashMap<String, Status>();
+ Map<String, Status> pipelineExpectedStateMap =
+ new HashMap<String, Status>();
+ Map<String, ExecutableNode> previousNodeMap =
+ new HashMap<String, ExecutableNode>();
+ Map<String, ExecutableNode> pipelineNodeMap =
+ new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow pipelineFlow = pipelineRunner.getExecutableFlow();
+ ExecutableFlow previousFlow = previousRunner.getExecutableFlow();
+ createExpectedStateMap(previousFlow, previousExpectedStateMap,
+ previousNodeMap);
+ createExpectedStateMap(pipelineFlow, pipelineExpectedStateMap,
+ pipelineNodeMap);
+
+ Thread thread1 = runFlowRunnerInThread(previousRunner);
+ pause(250);
+ Thread thread2 = runFlowRunnerInThread(pipelineRunner);
+ pause(250);
+
+ previousExpectedStateMap.put("pipeline1", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline1").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline1", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline2", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline2").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline2", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3", Status.RUNNING);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobA",
+ Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline1").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline1", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline2", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipelineEmbeddedFlow3:innerJobA")
+ .succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobA",
+ Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobB",
+ Status.RUNNING);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobC",
+ Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline2", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline2").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline2", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobA",
+ Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipelineEmbeddedFlow3:innerJobB")
+ .succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobB",
+ Status.SUCCEEDED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipelineEmbeddedFlow3:innerJobC")
+ .succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerFlow",
+ Status.RUNNING);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobC",
+ Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobA",
+ Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipelineEmbeddedFlow3:innerJobA")
+ .succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobA",
+ Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobC",
+ Status.QUEUED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobB",
+ Status.QUEUED);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerFlow",
+ Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipelineEmbeddedFlow3:innerFlow")
+ .succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3:innerFlow",
+ Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipelineEmbeddedFlow3", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline4", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobC",
+ Status.RUNNING);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobB",
+ Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipelineEmbeddedFlow3:innerJobB")
+ .succeedJob();
+ InteractiveTestJob.getTestJob("pipe:pipelineEmbeddedFlow3:innerJobC")
+ .succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobC",
+ Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerJobB",
+ Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerFlow",
+ Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline4").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline4", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipelineFlow", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerFlow",
+ Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipelineFlow").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipelineFlow", Status.SUCCEEDED);
+ Assert.assertEquals(Status.SUCCEEDED, previousFlow.getStatus());
+ Assert.assertFalse(thread1.isAlive());
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipelineEmbeddedFlow3:innerFlow")
+ .succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3:innerFlow",
+ Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineEmbeddedFlow3", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline4", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline4").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline4", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipelineFlow", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipelineFlow").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipelineFlow", Status.SUCCEEDED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+ Assert.assertEquals(Status.SUCCEEDED, pipelineFlow.getStatus());
+ Assert.assertFalse(thread2.isAlive());
+ }
+
+ @Ignore @Test
+ public void testBasicPipelineLevel2Run2() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner previousRunner =
+ createFlowRunner(eventCollector, "pipeline1_2", "prev");
+
+ ExecutionOptions options = new ExecutionOptions();
+ options.setPipelineExecutionId(previousRunner.getExecutableFlow()
+ .getExecutionId());
+ options.setPipelineLevel(2);
+ FlowWatcher watcher = new LocalFlowWatcher(previousRunner);
+ FlowRunner pipelineRunner =
+ createFlowRunner(eventCollector, "pipeline1_2", "pipe", options);
+ pipelineRunner.setFlowWatcher(watcher);
+
+ Map<String, Status> previousExpectedStateMap =
+ new HashMap<String, Status>();
+ Map<String, Status> pipelineExpectedStateMap =
+ new HashMap<String, Status>();
+ Map<String, ExecutableNode> previousNodeMap =
+ new HashMap<String, ExecutableNode>();
+ Map<String, ExecutableNode> pipelineNodeMap =
+ new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow pipelineFlow = pipelineRunner.getExecutableFlow();
+ ExecutableFlow previousFlow = previousRunner.getExecutableFlow();
+ createExpectedStateMap(previousFlow, previousExpectedStateMap,
+ previousNodeMap);
+ createExpectedStateMap(pipelineFlow, pipelineExpectedStateMap,
+ pipelineNodeMap);
+
+ Thread thread1 = runFlowRunnerInThread(previousRunner);
+ pause(250);
+ Thread thread2 = runFlowRunnerInThread(pipelineRunner);
+ pause(250);
+
+ previousExpectedStateMap.put("pipeline1_1", Status.RUNNING);
+ previousExpectedStateMap.put("pipeline1_1:innerJobA", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1_1", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1_1:innerJobA", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline1_1:innerJobA").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline1_1:innerJobA", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline1_1:innerFlow2", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline1_1:innerFlow2").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline1_1", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline1_1:innerFlow2", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline1_2", Status.RUNNING);
+ previousExpectedStateMap.put("pipeline1_2:innerJobA", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1_1:innerJobA", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline1_1:innerJobA").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline1_1:innerJobA", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_1:innerFlow2", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline1_2:innerJobA").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline1_2:innerJobA", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline1_2:innerFlow2", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1_1:innerFlow2", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline1_1:innerFlow2").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline1_1", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_1:innerFlow2", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_2", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1_2:innerJobA", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline1_1:innerFlow2").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline1_1", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_1:innerFlow2", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_2", Status.RUNNING);
+ pipelineExpectedStateMap.put("pipeline1_2:innerJobA", Status.QUEUED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("prev:pipeline1_2:innerFlow2").succeedJob();
+ pause(250);
+ previousExpectedStateMap.put("pipeline1_2:innerFlow2", Status.SUCCEEDED);
+ previousExpectedStateMap.put("pipeline1_2", Status.SUCCEEDED);
+ Assert.assertEquals(Status.SUCCEEDED, previousFlow.getStatus());
+ Assert.assertFalse(thread1.isAlive());
+ pipelineExpectedStateMap.put("pipeline1_2:innerJobA", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline1_2:innerJobA").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline1_2:innerJobA", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_2:innerFlow2", Status.RUNNING);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+
+ InteractiveTestJob.getTestJob("pipe:pipeline1_2:innerFlow2").succeedJob();
+ pause(250);
+ pipelineExpectedStateMap.put("pipeline1_2", Status.SUCCEEDED);
+ pipelineExpectedStateMap.put("pipeline1_2:innerFlow2", Status.SUCCEEDED);
+ compareStates(previousExpectedStateMap, previousNodeMap);
+ compareStates(pipelineExpectedStateMap, pipelineNodeMap);
+ Assert.assertEquals(Status.SUCCEEDED, pipelineFlow.getStatus());
+ Assert.assertFalse(thread2.isAlive());
+ }
+
+ private Thread runFlowRunnerInThread(FlowRunner runner) {
+ Thread thread = new Thread(runner);
+ thread.start();
+ return thread;
+ }
+
+ private void pause(long millisec) {
+ synchronized (this) {
+ try {
+ wait(millisec);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ private void createExpectedStateMap(ExecutableFlowBase flow,
+ Map<String, Status> expectedStateMap, Map<String, ExecutableNode> nodeMap) {
+ for (ExecutableNode node : flow.getExecutableNodes()) {
+ expectedStateMap.put(node.getNestedId(), node.getStatus());
+ nodeMap.put(node.getNestedId(), node);
+
+ if (node instanceof ExecutableFlowBase) {
+ createExpectedStateMap((ExecutableFlowBase) node, expectedStateMap,
+ nodeMap);
+ }
+ }
+ }
+
+ private void compareStates(Map<String, Status> expectedStateMap,
+ Map<String, ExecutableNode> nodeMap) {
+ for (String printedId : expectedStateMap.keySet()) {
+ Status expectedStatus = expectedStateMap.get(printedId);
+ ExecutableNode node = nodeMap.get(printedId);
+ if (node == null) {
+ System.out.println("id node: " + printedId + " doesn't exist.");
+ }
+ if (expectedStatus != node.getStatus()) {
+ Assert.fail("Expected values do not match for " + printedId
+ + ". Expected " + expectedStatus + ", instead received "
+ + node.getStatus());
+ }
+ }
+ }
+
+ private void prepareProject(File directory) throws ProjectManagerException,
+ IOException {
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+ loader.loadProjectFlow(directory);
+ if (!loader.getErrors().isEmpty()) {
+ for (String error : loader.getErrors()) {
+ System.out.println(error);
+ }
+
+ throw new RuntimeException("Errors found in setup");
+ }
+
+ flowMap = loader.getFlowMap();
+ project.setFlows(flowMap);
+ FileUtils.copyDirectory(directory, workingDir);
+ }
+
+ // private void printCurrentState(String prefix, ExecutableFlowBase flow) {
+ // for (ExecutableNode node: flow.getExecutableNodes()) {
+ // System.err.println(prefix + node.getNestedId() + "->" +
+ // node.getStatus().name());
+ // if (node instanceof ExecutableFlowBase) {
+ // printCurrentState(prefix, (ExecutableFlowBase)node);
+ // }
+ // }
+ // }
+ //
+ private FlowRunner createFlowRunner(EventCollectorListener eventCollector,
+ String flowName, String groupName) throws Exception {
+ return createFlowRunner(eventCollector, flowName, groupName,
+ new ExecutionOptions());
+ }
+
+ private FlowRunner createFlowRunner(EventCollectorListener eventCollector,
+ String flowName, String groupName, ExecutionOptions options)
+ throws Exception {
+ Flow flow = flowMap.get(flowName);
+
+ int exId = id++;
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+ exFlow.setExecutionPath(workingDir.getPath());
+ exFlow.setExecutionId(exId);
+
+ Map<String, String> flowParam = new HashMap<String, String>();
+ flowParam.put("group", groupName);
+ options.addAllFlowParameters(flowParam);
+ exFlow.setExecutionOptions(options);
+ fakeExecutorLoader.uploadExecutableFlow(exFlow);
+
+ FlowRunner runner =
+ new FlowRunner(fakeExecutorLoader.fetchExecutableFlow(exId),
+ fakeExecutorLoader, fakeProjectLoader, jobtypeManager);
+
+ runner.addListener(eventCollector);
+
+ return runner;
+ }
+
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerPropertyResolutionTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerPropertyResolutionTest.java
new file mode 100644
index 0000000..4642833
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerPropertyResolutionTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableFlowBase;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.InteractiveTestJob;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.flow.Flow;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.project.Project;
+import azkaban.project.ProjectLoader;
+import azkaban.project.ProjectManagerException;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.DirectoryFlowLoader;
+import azkaban.utils.Props;
+
+/**
+ * Test the property resolution of jobs in a flow.
+ *
+ * The tests are contained in execpropstest, and should be resolved in the
+ * following fashion, where the later props take precedence over the previous
+ * ones.
+ *
+ * 1. Global props (set in the FlowRunner)
+ * 2. Shared job props (depends on job directory)
+ * 3. Flow Override properties
+ * 4. Previous job outputs to the embedded flow (Only if contained in embedded flow)
+ * 5. Embedded flow properties (Only if contained in embedded flow)
+ * 6. Previous job outputs (if exists)
+ * 7. Job Props
+ *
+ * The test contains the following structure:
+ * job2 -> innerFlow (job1 -> job4 ) -> job3
+ *
+ * job2 and 4 are in nested directories so should have different shared
+ * properties than other jobs.
+ */
+public class FlowRunnerPropertyResolutionTest {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private ProjectLoader fakeProjectLoader;
+ private ExecutorLoader fakeExecutorLoader;
+ private Logger logger = Logger.getLogger(FlowRunnerTest2.class);
+ private Project project;
+ private Map<String, Flow> flowMap;
+ private static int id = 101;
+
+ @Before
+ public void setUp() throws Exception {
+ System.out.println("Create temp dir");
+ workingDir = new File("_AzkabanTestDir_" + System.currentTimeMillis());
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ jobtypeManager =
+ new JobTypeManager(null, null, this.getClass().getClassLoader());
+ jobtypeManager.getJobTypePluginSet().addPluginClass("java", JavaJob.class);
+ jobtypeManager.getJobTypePluginSet().addPluginClass("test",
+ InteractiveTestJob.class);
+ fakeProjectLoader = new MockProjectLoader(workingDir);
+ fakeExecutorLoader = new MockExecutorLoader();
+ project = new Project(1, "testProject");
+
+ File dir = new File("unit/executions/execpropstest");
+ prepareProject(dir);
+
+ InteractiveTestJob.clearTestJobs();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ System.out.println("Teardown temp dir");
+ if (workingDir != null) {
+ FileUtils.deleteDirectory(workingDir);
+ workingDir = null;
+ }
+ }
+
+ /**
+ * Tests the basic flow resolution. Flow is defined in execpropstest
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testPropertyResolution() throws Exception {
+ HashMap<String, String> flowProps = new HashMap<String, String>();
+ flowProps.put("props7", "flow7");
+ flowProps.put("props6", "flow6");
+ flowProps.put("props5", "flow5");
+ FlowRunner runner = createFlowRunner("job3", flowProps);
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+ createNodeMap(runner.getExecutableFlow(), nodeMap);
+
+ // 1. Start flow. Job 2 should start
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // Job 2 is a normal job.
+ // Only the flow overrides and the shared properties matter
+ ExecutableNode node = nodeMap.get("job2");
+ Props job2Props = node.getInputProps();
+ Assert.assertEquals("shared1", job2Props.get("props1"));
+ Assert.assertEquals("job2", job2Props.get("props2"));
+ Assert.assertEquals("moo3", job2Props.get("props3"));
+ Assert.assertEquals("job7", job2Props.get("props7"));
+ Assert.assertEquals("flow5", job2Props.get("props5"));
+ Assert.assertEquals("flow6", job2Props.get("props6"));
+ Assert.assertEquals("shared4", job2Props.get("props4"));
+ Assert.assertEquals("shared8", job2Props.get("props8"));
+
+ // Job 1 is inside another flow, and is nested in a different directory
+ // The priority order should be:
+ // job1->innerflow->job2.output->flow.overrides->job1 shared props
+ Props job2Generated = new Props();
+ job2Generated.put("props6", "gjob6");
+ job2Generated.put("props9", "gjob9");
+ job2Generated.put("props10", "gjob10");
+ InteractiveTestJob.getTestJob("job2").succeedJob(job2Generated);
+ pause(250);
+ node = nodeMap.get("innerflow:job1");
+ Props job1Props = node.getInputProps();
+ Assert.assertEquals("job1", job1Props.get("props1"));
+ Assert.assertEquals("job2", job1Props.get("props2"));
+ Assert.assertEquals("job8", job1Props.get("props8"));
+ Assert.assertEquals("gjob9", job1Props.get("props9"));
+ Assert.assertEquals("gjob10", job1Props.get("props10"));
+ Assert.assertEquals("innerflow6", job1Props.get("props6"));
+ Assert.assertEquals("innerflow5", job1Props.get("props5"));
+ Assert.assertEquals("flow7", job1Props.get("props7"));
+ Assert.assertEquals("moo3", job1Props.get("props3"));
+ Assert.assertEquals("moo4", job1Props.get("props4"));
+
+ // Job 4 is inside another flow and takes output from job 1
+ // The priority order should be:
+ // job4->job1.output->innerflow->job2.output->flow.overrides->job4 shared
+ // props
+ Props job1GeneratedProps = new Props();
+ job1GeneratedProps.put("props9", "g2job9");
+ job1GeneratedProps.put("props7", "g2job7");
+ InteractiveTestJob.getTestJob("innerflow:job1").succeedJob(
+ job1GeneratedProps);
+ pause(250);
+ node = nodeMap.get("innerflow:job4");
+ Props job4Props = node.getInputProps();
+ Assert.assertEquals("job8", job4Props.get("props8"));
+ Assert.assertEquals("job9", job4Props.get("props9"));
+ Assert.assertEquals("g2job7", job4Props.get("props7"));
+ Assert.assertEquals("innerflow5", job4Props.get("props5"));
+ Assert.assertEquals("innerflow6", job4Props.get("props6"));
+ Assert.assertEquals("gjob10", job4Props.get("props10"));
+ Assert.assertEquals("shared4", job4Props.get("props4"));
+ Assert.assertEquals("shared1", job4Props.get("props1"));
+ Assert.assertEquals("shared2", job4Props.get("props2"));
+ Assert.assertEquals("moo3", job4Props.get("props3"));
+
+ // Job 3 is a normal job taking props from an embedded flow
+ // The priority order should be:
+ // job3->innerflow.output->flow.overrides->job3.sharedprops
+ Props job4GeneratedProps = new Props();
+ job4GeneratedProps.put("props9", "g4job9");
+ job4GeneratedProps.put("props6", "g4job6");
+ InteractiveTestJob.getTestJob("innerflow:job4").succeedJob(
+ job4GeneratedProps);
+ pause(250);
+ node = nodeMap.get("job3");
+ Props job3Props = node.getInputProps();
+ Assert.assertEquals("job3", job3Props.get("props3"));
+ Assert.assertEquals("g4job6", job3Props.get("props6"));
+ Assert.assertEquals("g4job9", job3Props.get("props9"));
+ Assert.assertEquals("flow7", job3Props.get("props7"));
+ Assert.assertEquals("flow5", job3Props.get("props5"));
+ Assert.assertEquals("shared1", job3Props.get("props1"));
+ Assert.assertEquals("shared2", job3Props.get("props2"));
+ Assert.assertEquals("moo4", job3Props.get("props4"));
+ }
+
+ private void prepareProject(File directory) throws ProjectManagerException,
+ IOException {
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+ loader.loadProjectFlow(directory);
+ if (!loader.getErrors().isEmpty()) {
+ for (String error : loader.getErrors()) {
+ System.out.println(error);
+ }
+
+ throw new RuntimeException("Errors found in setup");
+ }
+
+ flowMap = loader.getFlowMap();
+ project.setFlows(flowMap);
+ FileUtils.copyDirectory(directory, workingDir);
+ }
+
+ private FlowRunner createFlowRunner(String flowName,
+ HashMap<String, String> flowParams) throws Exception {
+ Flow flow = flowMap.get(flowName);
+
+ int exId = id++;
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+ exFlow.setExecutionPath(workingDir.getPath());
+ exFlow.setExecutionId(exId);
+
+ exFlow.getExecutionOptions().addAllFlowParameters(flowParams);
+ fakeExecutorLoader.uploadExecutableFlow(exFlow);
+
+ FlowRunner runner =
+ new FlowRunner(fakeExecutorLoader.fetchExecutableFlow(exId),
+ fakeExecutorLoader, fakeProjectLoader, jobtypeManager);
+ return runner;
+ }
+
+ private void createNodeMap(ExecutableFlowBase flow,
+ Map<String, ExecutableNode> nodeMap) {
+ for (ExecutableNode node : flow.getExecutableNodes()) {
+ nodeMap.put(node.getNestedId(), node);
+
+ if (node instanceof ExecutableFlowBase) {
+ createNodeMap((ExecutableFlowBase) node, nodeMap);
+ }
+ }
+ }
+
+ private Thread runFlowRunnerInThread(FlowRunner runner) {
+ Thread thread = new Thread(runner);
+ thread.start();
+ return thread;
+ }
+
+ private void pause(long millisec) {
+ synchronized (this) {
+ try {
+ wait(millisec);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerTest.java
new file mode 100644
index 0000000..8b2c6d4
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.apache.commons.io.FileUtils;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.event.Event;
+import azkaban.event.Event.Type;
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutionOptions.FailureAction;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.InteractiveTestJob;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.executor.Status;
+import azkaban.flow.Flow;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.jobtype.JobTypePluginSet;
+import azkaban.project.Project;
+import azkaban.project.ProjectLoader;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.JSONUtils;
+
+public class FlowRunnerTest {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private ProjectLoader fakeProjectLoader;
+
+ public FlowRunnerTest() {
+
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ System.out.println("Create temp dir");
+ synchronized (this) {
+ workingDir = new File("_AzkabanTestDir_" + System.currentTimeMillis());
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ }
+ jobtypeManager =
+ new JobTypeManager(null, null, this.getClass().getClassLoader());
+ JobTypePluginSet pluginSet = jobtypeManager.getJobTypePluginSet();
+ pluginSet.addPluginClass("java", JavaJob.class);
+ pluginSet.addPluginClass("test", InteractiveTestJob.class);
+ fakeProjectLoader = new MockProjectLoader(workingDir);
+
+ InteractiveTestJob.clearTestJobs();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ System.out.println("Teardown temp dir");
+ synchronized (this) {
+ if (workingDir != null) {
+ FileUtils.deleteDirectory(workingDir);
+ workingDir = null;
+ }
+ }
+ }
+
+ @Ignore @Test
+ public void exec1Normal() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ // just making compile. may not work at all.
+
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ FlowRunner runner = createFlowRunner(loader, eventCollector, "exec1");
+
+ Assert.assertTrue(!runner.isKilled());
+ runner.run();
+ ExecutableFlow exFlow = runner.getExecutableFlow();
+ Assert.assertTrue(exFlow.getStatus() == Status.SUCCEEDED);
+ compareFinishedRuntime(runner);
+
+ testStatus(exFlow, "job1", Status.SUCCEEDED);
+ testStatus(exFlow, "job2", Status.SUCCEEDED);
+ testStatus(exFlow, "job3", Status.SUCCEEDED);
+ testStatus(exFlow, "job4", Status.SUCCEEDED);
+ testStatus(exFlow, "job5", Status.SUCCEEDED);
+ testStatus(exFlow, "job6", Status.SUCCEEDED);
+ testStatus(exFlow, "job7", Status.SUCCEEDED);
+ testStatus(exFlow, "job8", Status.SUCCEEDED);
+ testStatus(exFlow, "job10", Status.SUCCEEDED);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.FLOW_STARTED,
+ Type.FLOW_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void exec1Disabled() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow exFlow = prepareExecDir(testDir, "exec1", 1);
+
+ // Disable couple in the middle and at the end.
+ exFlow.getExecutableNode("job1").setStatus(Status.DISABLED);
+ exFlow.getExecutableNode("job6").setStatus(Status.DISABLED);
+ exFlow.getExecutableNode("job5").setStatus(Status.DISABLED);
+ exFlow.getExecutableNode("job10").setStatus(Status.DISABLED);
+
+ FlowRunner runner = createFlowRunner(exFlow, loader, eventCollector);
+
+ Assert.assertTrue(!runner.isKilled());
+ Assert.assertTrue(exFlow.getStatus() == Status.READY);
+ runner.run();
+
+ exFlow = runner.getExecutableFlow();
+ compareFinishedRuntime(runner);
+
+ Assert.assertTrue(exFlow.getStatus() == Status.SUCCEEDED);
+
+ testStatus(exFlow, "job1", Status.SKIPPED);
+ testStatus(exFlow, "job2", Status.SUCCEEDED);
+ testStatus(exFlow, "job3", Status.SUCCEEDED);
+ testStatus(exFlow, "job4", Status.SUCCEEDED);
+ testStatus(exFlow, "job5", Status.SKIPPED);
+ testStatus(exFlow, "job6", Status.SKIPPED);
+ testStatus(exFlow, "job7", Status.SUCCEEDED);
+ testStatus(exFlow, "job8", Status.SUCCEEDED);
+ testStatus(exFlow, "job10", Status.SKIPPED);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.FLOW_STARTED,
+ Type.FLOW_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void exec1Failed() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow flow = prepareExecDir(testDir, "exec2", 1);
+
+ FlowRunner runner = createFlowRunner(flow, loader, eventCollector);
+
+ runner.run();
+ ExecutableFlow exFlow = runner.getExecutableFlow();
+ Assert.assertTrue(!runner.isKilled());
+ Assert.assertTrue("Flow status " + exFlow.getStatus(),
+ exFlow.getStatus() == Status.FAILED);
+
+ testStatus(exFlow, "job1", Status.SUCCEEDED);
+ testStatus(exFlow, "job2d", Status.FAILED);
+ testStatus(exFlow, "job3", Status.CANCELLED);
+ testStatus(exFlow, "job4", Status.CANCELLED);
+ testStatus(exFlow, "job5", Status.CANCELLED);
+ testStatus(exFlow, "job6", Status.SUCCEEDED);
+ testStatus(exFlow, "job7", Status.CANCELLED);
+ testStatus(exFlow, "job8", Status.CANCELLED);
+ testStatus(exFlow, "job9", Status.CANCELLED);
+ testStatus(exFlow, "job10", Status.CANCELLED);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.FLOW_STARTED,
+ Type.FLOW_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void exec1FailedKillAll() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow flow = prepareExecDir(testDir, "exec2", 1);
+ flow.getExecutionOptions().setFailureAction(FailureAction.CANCEL_ALL);
+
+ FlowRunner runner = createFlowRunner(flow, loader, eventCollector);
+
+ runner.run();
+ ExecutableFlow exFlow = runner.getExecutableFlow();
+
+ Assert.assertTrue(runner.isKilled());
+
+ Assert.assertTrue(
+ "Expected flow " + Status.FAILED + " instead " + exFlow.getStatus(),
+ exFlow.getStatus() == Status.FAILED);
+
+ synchronized (this) {
+ try {
+ wait(500);
+ } catch (InterruptedException e) {
+
+ }
+ }
+
+ testStatus(exFlow, "job1", Status.SUCCEEDED);
+ testStatus(exFlow, "job2d", Status.FAILED);
+ testStatus(exFlow, "job3", Status.CANCELLED);
+ testStatus(exFlow, "job4", Status.CANCELLED);
+ testStatus(exFlow, "job5", Status.CANCELLED);
+ testStatus(exFlow, "job6", Status.KILLED);
+ testStatus(exFlow, "job7", Status.CANCELLED);
+ testStatus(exFlow, "job8", Status.CANCELLED);
+ testStatus(exFlow, "job9", Status.CANCELLED);
+ testStatus(exFlow, "job10", Status.CANCELLED);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.FLOW_STARTED,
+ Type.FLOW_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ eventCollector.writeAllEvents();
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void exec1FailedFinishRest() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow flow = prepareExecDir(testDir, "exec3", 1);
+ flow.getExecutionOptions().setFailureAction(
+ FailureAction.FINISH_ALL_POSSIBLE);
+ FlowRunner runner = createFlowRunner(flow, loader, eventCollector);
+
+ runner.run();
+ ExecutableFlow exFlow = runner.getExecutableFlow();
+ Assert.assertTrue(
+ "Expected flow " + Status.FAILED + " instead " + exFlow.getStatus(),
+ exFlow.getStatus() == Status.FAILED);
+
+ synchronized (this) {
+ try {
+ wait(500);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ testStatus(exFlow, "job1", Status.SUCCEEDED);
+ testStatus(exFlow, "job2d", Status.FAILED);
+ testStatus(exFlow, "job3", Status.SUCCEEDED);
+ testStatus(exFlow, "job4", Status.CANCELLED);
+ testStatus(exFlow, "job5", Status.CANCELLED);
+ testStatus(exFlow, "job6", Status.CANCELLED);
+ testStatus(exFlow, "job7", Status.SUCCEEDED);
+ testStatus(exFlow, "job8", Status.SUCCEEDED);
+ testStatus(exFlow, "job9", Status.SUCCEEDED);
+ testStatus(exFlow, "job10", Status.CANCELLED);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.FLOW_STARTED,
+ Type.FLOW_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ eventCollector.writeAllEvents();
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void execAndCancel() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ FlowRunner runner = createFlowRunner(loader, eventCollector, "exec1");
+
+ Assert.assertTrue(!runner.isKilled());
+ Thread thread = new Thread(runner);
+ thread.start();
+
+ synchronized (this) {
+ try {
+ wait(5000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ runner.kill("me");
+ Assert.assertTrue(runner.isKilled());
+ }
+
+ synchronized (this) {
+ // Wait for cleanup.
+ try {
+ wait(2000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ ExecutableFlow exFlow = runner.getExecutableFlow();
+ testStatus(exFlow, "job1", Status.SUCCEEDED);
+ testStatus(exFlow, "job2", Status.SUCCEEDED);
+ testStatus(exFlow, "job5", Status.CANCELLED);
+ testStatus(exFlow, "job7", Status.CANCELLED);
+ testStatus(exFlow, "job8", Status.CANCELLED);
+ testStatus(exFlow, "job10", Status.CANCELLED);
+ testStatus(exFlow, "job3", Status.KILLED);
+ testStatus(exFlow, "job4", Status.KILLED);
+ testStatus(exFlow, "job6", Status.KILLED);
+
+ Assert.assertTrue(
+ "Expected FAILED status instead got " + exFlow.getStatus(),
+ exFlow.getStatus() == Status.KILLED);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.FLOW_STARTED,
+ Type.FLOW_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ eventCollector.writeAllEvents();
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void execRetries() throws Exception {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ eventCollector.setEventFilterOut(Event.Type.JOB_FINISHED,
+ Event.Type.JOB_STARTED, Event.Type.JOB_STATUS_CHANGED);
+ FlowRunner runner = createFlowRunner(loader, eventCollector, "exec4-retry");
+
+ runner.run();
+
+ ExecutableFlow exFlow = runner.getExecutableFlow();
+ testStatus(exFlow, "job-retry", Status.SUCCEEDED);
+ testStatus(exFlow, "job-pass", Status.SUCCEEDED);
+ testStatus(exFlow, "job-retry-fail", Status.FAILED);
+ testAttempts(exFlow, "job-retry", 3);
+ testAttempts(exFlow, "job-pass", 0);
+ testAttempts(exFlow, "job-retry-fail", 2);
+
+ Assert.assertTrue(
+ "Expected FAILED status instead got " + exFlow.getStatus(),
+ exFlow.getStatus() == Status.FAILED);
+ }
+
+ private void testStatus(ExecutableFlow flow, String name, Status status) {
+ ExecutableNode node = flow.getExecutableNode(name);
+
+ if (node.getStatus() != status) {
+ Assert.fail("Status of job " + node.getId() + " is " + node.getStatus()
+ + " not " + status + " as expected.");
+ }
+ }
+
+ private void testAttempts(ExecutableFlow flow, String name, int attempt) {
+ ExecutableNode node = flow.getExecutableNode(name);
+
+ if (node.getAttempt() != attempt) {
+ Assert.fail("Expected " + attempt + " got " + node.getAttempt()
+ + " attempts " + name);
+ }
+ }
+
+ private ExecutableFlow prepareExecDir(File execDir, String flowName,
+ int execId) throws IOException {
+ synchronized (this) {
+ FileUtils.copyDirectory(execDir, workingDir);
+ }
+
+ File jsonFlowFile = new File(workingDir, flowName + ".flow");
+ @SuppressWarnings("unchecked")
+ HashMap<String, Object> flowObj =
+ (HashMap<String, Object>) JSONUtils.parseJSONFromFile(jsonFlowFile);
+
+ Project project = new Project(1, "myproject");
+ project.setVersion(2);
+
+ Flow flow = Flow.flowFromObject(flowObj);
+ ExecutableFlow execFlow = new ExecutableFlow(project, flow);
+ execFlow.setExecutionId(execId);
+ execFlow.setExecutionPath(workingDir.getPath());
+ return execFlow;
+ }
+
+ private void compareFinishedRuntime(FlowRunner runner) throws Exception {
+ ExecutableFlow flow = runner.getExecutableFlow();
+ for (String flowName : flow.getStartNodes()) {
+ ExecutableNode node = flow.getExecutableNode(flowName);
+ compareStartFinishTimes(flow, node, 0);
+ }
+ }
+
+ private void compareStartFinishTimes(ExecutableFlow flow,
+ ExecutableNode node, long previousEndTime) throws Exception {
+ long startTime = node.getStartTime();
+ long endTime = node.getEndTime();
+
+ // If start time is < 0, so will the endtime.
+ if (startTime <= 0) {
+ Assert.assertTrue(endTime <= 0);
+ return;
+ }
+
+ // System.out.println("Node " + node.getJobId() + " start:" + startTime +
+ // " end:" + endTime + " previous:" + previousEndTime);
+ Assert.assertTrue("Checking start and end times", startTime > 0
+ && endTime >= startTime);
+ Assert.assertTrue("Start time for " + node.getId() + " is " + startTime
+ + " and less than " + previousEndTime, startTime >= previousEndTime);
+
+ for (String outNode : node.getOutNodes()) {
+ ExecutableNode childNode = flow.getExecutableNode(outNode);
+ compareStartFinishTimes(flow, childNode, endTime);
+ }
+ }
+
+ private FlowRunner createFlowRunner(ExecutableFlow flow,
+ ExecutorLoader loader, EventCollectorListener eventCollector)
+ throws Exception {
+ // File testDir = new File("unit/executions/exectest1");
+ // MockProjectLoader projectLoader = new MockProjectLoader(new
+ // File(flow.getExecutionPath()));
+
+ loader.uploadExecutableFlow(flow);
+ FlowRunner runner =
+ new FlowRunner(flow, loader, fakeProjectLoader, jobtypeManager);
+
+ runner.addListener(eventCollector);
+
+ return runner;
+ }
+
+ private FlowRunner createFlowRunner(ExecutorLoader loader,
+ EventCollectorListener eventCollector, String flowName) throws Exception {
+ File testDir = new File("unit/executions/exectest1");
+ ExecutableFlow exFlow = prepareExecDir(testDir, flowName, 1);
+ // MockProjectLoader projectLoader = new MockProjectLoader(new
+ // File(exFlow.getExecutionPath()));
+
+ loader.uploadExecutableFlow(exFlow);
+
+ FlowRunner runner =
+ new FlowRunner(exFlow, loader, fakeProjectLoader, jobtypeManager);
+
+ runner.addListener(eventCollector);
+
+ return runner;
+ }
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerTest2.java b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerTest2.java
new file mode 100644
index 0000000..fad4450
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/FlowRunnerTest2.java
@@ -0,0 +1,1430 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+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.Ignore;
+import org.junit.Test;
+
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableFlowBase;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutionOptions.FailureAction;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.InteractiveTestJob;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.executor.Status;
+import azkaban.flow.Flow;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.jobtype.JobTypePluginSet;
+import azkaban.project.Project;
+import azkaban.project.ProjectLoader;
+import azkaban.project.ProjectManagerException;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.DirectoryFlowLoader;
+import azkaban.utils.Props;
+
+/**
+ * Test the flow run, especially with embedded flows.
+ *
+ * This test uses executions/embedded2. It also mainly uses the flow named
+ * jobf. The test is designed to control success/failures explicitly so we
+ * don't have to time the flow exactly.
+ *
+ * Flow jobf looks like the following:
+ *
+ *
+ * joba joba1
+ * / | \ |
+ * / | \ |
+ * jobb jobd jobc |
+ * \ | / /
+ * \ | / /
+ * jobe /
+ * | /
+ * | /
+ * jobf
+ *
+ * The job 'jobb' is an embedded flow:
+ *
+ * jobb:innerFlow
+ *
+ * innerJobA
+ * / \
+ * innerJobB innerJobC
+ * \ /
+ * innerFlow
+ *
+ *
+ * The job 'jobd' is a simple embedded flow:
+ *
+ * jobd:innerFlow2
+ *
+ * innerJobA
+ * |
+ * innerFlow2
+ *
+ * The following tests checks each stage of the flow run by forcing jobs to
+ * succeed or fail.
+ */
+public class FlowRunnerTest2 {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private ProjectLoader fakeProjectLoader;
+ private ExecutorLoader fakeExecutorLoader;
+ private Logger logger = Logger.getLogger(FlowRunnerTest2.class);
+ private Project project;
+ private Map<String, Flow> flowMap;
+ private static int id=101;
+
+ public FlowRunnerTest2() {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ System.out.println("Create temp dir");
+ workingDir = new File("_AzkabanTestDir_" + System.currentTimeMillis());
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ jobtypeManager = new JobTypeManager(null, null,
+ this.getClass().getClassLoader());
+ JobTypePluginSet pluginSet = jobtypeManager.getJobTypePluginSet();
+
+ pluginSet.addPluginClass("java", JavaJob.class);
+ pluginSet.addPluginClass("test", InteractiveTestJob.class);
+ fakeProjectLoader = new MockProjectLoader(workingDir);
+ fakeExecutorLoader = new MockExecutorLoader();
+ project = new Project(1, "testProject");
+
+ File dir = new File("unit/executions/embedded2");
+ prepareProject(dir);
+
+ InteractiveTestJob.clearTestJobs();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ System.out.println("Teardown temp dir");
+ if (workingDir != null) {
+ FileUtils.deleteDirectory(workingDir);
+ workingDir = null;
+ }
+ }
+
+ /**
+ * Tests the basic successful flow run, and also tests all output variables
+ * from each job.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testBasicRun() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow flow = runner.getExecutableFlow();
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+
+ compareStates(expectedStateMap, nodeMap);
+ Props joba = nodeMap.get("joba").getInputProps();
+ Assert.assertEquals("joba.1", joba.get("param1"));
+ Assert.assertEquals("test1.2", joba.get("param2"));
+ Assert.assertEquals("test1.3", joba.get("param3"));
+ Assert.assertEquals("override.4", joba.get("param4"));
+ Assert.assertEquals("test2.5", joba.get("param5"));
+ Assert.assertEquals("test2.6", joba.get("param6"));
+ Assert.assertEquals("test2.7", joba.get("param7"));
+ Assert.assertEquals("test2.8", joba.get("param8"));
+
+ Props joba1 = nodeMap.get("joba1").getInputProps();
+ Assert.assertEquals("test1.1", joba1.get("param1"));
+ Assert.assertEquals("test1.2", joba1.get("param2"));
+ Assert.assertEquals("test1.3", joba1.get("param3"));
+ Assert.assertEquals("override.4", joba1.get("param4"));
+ Assert.assertEquals("test2.5", joba1.get("param5"));
+ Assert.assertEquals("test2.6", joba1.get("param6"));
+ Assert.assertEquals("test2.7", joba1.get("param7"));
+ Assert.assertEquals("test2.8", joba1.get("param8"));
+
+ // 2. JOB A COMPLETES SUCCESSFULLY
+ InteractiveTestJob.getTestJob("joba").succeedJob(
+ Props.of("output.joba", "joba", "output.override", "joba"));
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ ExecutableNode node = nodeMap.get("jobb");
+ Assert.assertEquals(Status.RUNNING, node.getStatus());
+ Props jobb = node.getInputProps();
+ Assert.assertEquals("override.4", jobb.get("param4"));
+ // Test that jobb properties overwrites the output properties
+ Assert.assertEquals("moo", jobb.get("testprops"));
+ Assert.assertEquals("jobb", jobb.get("output.override"));
+ Assert.assertEquals("joba", jobb.get("output.joba"));
+
+ Props jobbInnerJobA = nodeMap.get("jobb:innerJobA").getInputProps();
+ Assert.assertEquals("test1.1", jobbInnerJobA.get("param1"));
+ Assert.assertEquals("test1.2", jobbInnerJobA.get("param2"));
+ Assert.assertEquals("test1.3", jobbInnerJobA.get("param3"));
+ Assert.assertEquals("override.4", jobbInnerJobA.get("param4"));
+ Assert.assertEquals("test2.5", jobbInnerJobA.get("param5"));
+ Assert.assertEquals("test2.6", jobbInnerJobA.get("param6"));
+ Assert.assertEquals("test2.7", jobbInnerJobA.get("param7"));
+ Assert.assertEquals("test2.8", jobbInnerJobA.get("param8"));
+ Assert.assertEquals("joba", jobbInnerJobA.get("output.joba"));
+
+ // 3. jobb:Inner completes
+ /// innerJobA completes
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob(
+ Props.of("output.jobb.innerJobA", "jobb.innerJobA"));
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+ Props jobbInnerJobB = nodeMap.get("jobb:innerJobB").getInputProps();
+ Assert.assertEquals("test1.1", jobbInnerJobB.get("param1"));
+ Assert.assertEquals("override.4", jobbInnerJobB.get("param4"));
+ Assert.assertEquals("jobb.innerJobA",
+ jobbInnerJobB.get("output.jobb.innerJobA"));
+ Assert.assertEquals("moo", jobbInnerJobB.get("testprops"));
+ /// innerJobB, C completes
+ InteractiveTestJob.getTestJob("jobb:innerJobB").succeedJob(
+ Props.of("output.jobb.innerJobB", "jobb.innerJobB"));
+ InteractiveTestJob.getTestJob("jobb:innerJobC").succeedJob(
+ Props.of("output.jobb.innerJobC", "jobb.innerJobC"));
+ pause(250);
+ expectedStateMap.put("jobb:innerJobB", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerFlow", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ Props jobbInnerJobD = nodeMap.get("jobb:innerFlow").getInputProps();
+ Assert.assertEquals("test1.1", jobbInnerJobD.get("param1"));
+ Assert.assertEquals("override.4", jobbInnerJobD.get("param4"));
+ Assert.assertEquals("jobb.innerJobB",
+ jobbInnerJobD.get("output.jobb.innerJobB"));
+ Assert.assertEquals("jobb.innerJobC",
+ jobbInnerJobD.get("output.jobb.innerJobC"));
+
+ // 4. Finish up on inner flow for jobb
+ InteractiveTestJob.getTestJob("jobb:innerFlow").succeedJob(
+ Props.of("output1.jobb", "test1", "output2.jobb", "test2"));
+ pause(250);
+ expectedStateMap.put("jobb:innerFlow", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ Props jobbOutput = nodeMap.get("jobb").getOutputProps();
+ Assert.assertEquals("test1", jobbOutput.get("output1.jobb"));
+ Assert.assertEquals("test2", jobbOutput.get("output2.jobb"));
+
+ // 5. Finish jobc, jobd
+ InteractiveTestJob.getTestJob("jobc").succeedJob(
+ Props.of("output.jobc", "jobc"));
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("jobd:innerFlow2").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ expectedStateMap.put("jobd", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ Props jobd = nodeMap.get("jobe").getInputProps();
+ Assert.assertEquals("test1", jobd.get("output1.jobb"));
+ Assert.assertEquals("jobc", jobd.get("output.jobc"));
+
+ // 6. Finish off flow
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("jobe").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.SUCCEEDED);
+ expectedStateMap.put("jobf", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobf").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobf", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.SUCCEEDED, flow.getStatus());
+
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests a flow with Disabled jobs and flows. They should properly SKIP
+ * executions
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testDisabledNormal() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+ flow.getExecutableNode("jobb").setStatus(Status.DISABLED);
+ ((ExecutableFlowBase)flow.getExecutableNode("jobd")).getExecutableNode(
+ "innerJobA").setStatus(Status.DISABLED);
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY, others should be skipped
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.SKIPPED);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.SKIPPED);
+ expectedStateMap.put("jobd:innerFlow2", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.READY);
+ expectedStateMap.put("jobb:innerJobB", Status.READY);
+ expectedStateMap.put("jobb:innerJobC", Status.READY);
+ expectedStateMap.put("jobb:innerFlow", Status.READY);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 3. jobb:Inner completes
+ /// innerJobA completes
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerFlow2").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ expectedStateMap.put("jobd", Status.SUCCEEDED);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobe").succeedJob();
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobe", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobf", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 4. Finish up on inner flow for jobb
+ InteractiveTestJob.getTestJob("jobf").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobf", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+
+ Assert.assertEquals(Status.SUCCEEDED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests a failure with the default FINISH_CURRENTLY_RUNNING.
+ * After the first failure, every job that started should complete, and the
+ * rest of the jobs should be skipped.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testNormalFailure1() throws Exception {
+ // Test propagation of KILLED status to embedded flows.
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY, others should be skipped
+ InteractiveTestJob.getTestJob("joba").failJob();
+ pause(250);
+ Assert.assertEquals(Status.FAILED_FINISHING, flow.getStatus());
+ expectedStateMap.put("joba", Status.FAILED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.CANCELLED);
+ expectedStateMap.put("jobc", Status.CANCELLED);
+ expectedStateMap.put("jobd", Status.CANCELLED);
+ expectedStateMap.put("jobd:innerJobA", Status.READY);
+ expectedStateMap.put("jobd:innerFlow2", Status.READY);
+ expectedStateMap.put("jobb:innerJobA", Status.READY);
+ expectedStateMap.put("jobb:innerFlow", Status.READY);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 3. jobb:Inner completes
+ /// innerJobA completes
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Test #2 on the default failure case.
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testNormalFailure2() throws Exception {
+ // Test propagation of KILLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY, others should be skipped
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+
+ InteractiveTestJob.getTestJob("joba1").failJob();
+ pause(250);
+ expectedStateMap.put("joba1", Status.FAILED);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 3. joba completes, everything is killed
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerJobC", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobb", Status.KILLED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.FAILED_FINISHING, flow.getStatus());
+
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ @Ignore @Test
+ public void testNormalFailure3() throws Exception {
+ // Test propagation of CANCELLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB in subflow FAILS
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobB").failJob();
+ pause(250);
+ expectedStateMap.put("jobb", Status.FAILED_FINISHING);
+ expectedStateMap.put("jobb:innerJobB", Status.FAILED);
+ Assert.assertEquals(Status.FAILED_FINISHING, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobC").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ expectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobb", Status.FAILED);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 3. jobc completes, everything is killed
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests failures when the fail behaviour is FINISH_ALL_POSSIBLE.
+ * In this case, all jobs which have had its pre-requisite met can continue
+ * to run. Finishes when the failure is propagated to the last node of the
+ * flow.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testFailedFinishingFailure3() throws Exception {
+ // Test propagation of KILLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf",
+ FailureAction.FINISH_ALL_POSSIBLE);
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB in subflow FAILS
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobB").failJob();
+ pause(250);
+ expectedStateMap.put("jobb", Status.FAILED_FINISHING);
+ expectedStateMap.put("jobb:innerJobB", Status.FAILED);
+ Assert.assertEquals(Status.FAILED_FINISHING, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobC").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb", Status.FAILED);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerFlow2", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobd:innerFlow2").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ expectedStateMap.put("jobd", Status.SUCCEEDED);
+
+ // 3. jobc completes, everything is killed
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests the failure condition when a failure invokes a cancel (or killed)
+ * on the flow.
+ *
+ * Any jobs that are running will be assigned a KILLED state, and any nodes
+ * which were skipped due to prior errors will be given a CANCELLED state.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testCancelOnFailure() throws Exception {
+ // Test propagation of KILLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf",
+ FailureAction.CANCEL_ALL);
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB in subflow FAILS
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobB").failJob();
+ pause(250);
+ expectedStateMap.put("jobb", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobB", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobC", Status.KILLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobc", Status.KILLED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ expectedStateMap.put("jobd:innerJobA", Status.KILLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ compareStates(expectedStateMap, nodeMap);
+
+ Assert.assertFalse(thread.isAlive());
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+
+ }
+
+ /**
+ * Tests retries after a failure
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testRetryOnFailure() throws Exception {
+ // Test propagation of KILLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+ flow.getExecutableNode("joba").setStatus(Status.DISABLED);
+ ((ExecutableFlowBase)flow.getExecutableNode("jobb")).getExecutableNode(
+ "innerFlow").setStatus(Status.DISABLED);
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.SKIPPED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobB").failJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobC").failJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobB", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobC", Status.FAILED);
+ expectedStateMap.put("jobb:innerFlow", Status.SKIPPED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ Assert.assertEquals(Status.FAILED_FINISHING, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+
+ ExecutableNode node = nodeMap.get("jobd:innerFlow2");
+ ExecutableFlowBase base = node.getParentFlow();
+ for (String nodeId : node.getInNodes()) {
+ ExecutableNode inNode = base.getExecutableNode(nodeId);
+ System.out.println(inNode.getId() + " > " + inNode.getStatus());
+ }
+
+ runner.retryFailures("me");
+ pause(500);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerFlow", Status.DISABLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.RUNNING);
+ Assert.assertEquals(Status.RUNNING, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertTrue(thread.isAlive());
+
+
+ InteractiveTestJob.getTestJob("jobb:innerJobB").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobC").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerFlow2").succeedJob();
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerFlow", Status.SKIPPED);
+ expectedStateMap.put("jobb", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("jobd", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobe").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobe", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobf", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobf").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobf", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.SUCCEEDED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests the manual Killing of a flow. In this case, the flow is just fine
+ * before the cancel
+ * is called.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testCancel() throws Exception {
+ // Test propagation of KILLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf",
+ FailureAction.CANCEL_ALL);
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(1000);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB in subflow FAILS
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.kill("me");
+ pause(250);
+
+ expectedStateMap.put("jobb", Status.KILLED);
+ expectedStateMap.put("jobb:innerJobB", Status.KILLED);
+ expectedStateMap.put("jobb:innerJobC", Status.KILLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobc", Status.KILLED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ expectedStateMap.put("jobd:innerJobA", Status.KILLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+
+ Assert.assertEquals(Status.KILLED, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests the manual invocation of cancel on a flow that is FAILED_FINISHING
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testManualCancelOnFailure() throws Exception {
+ // Test propagation of KILLED status to embedded flows different branch
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+ ExecutableFlow flow = runner.getExecutableFlow();
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB in subflow FAILS
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobb:innerJobB").failJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobB", Status.FAILED);
+ expectedStateMap.put("jobb", Status.FAILED_FINISHING);
+ Assert.assertEquals(Status.FAILED_FINISHING, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.kill("me");
+ pause(1000);
+
+ expectedStateMap.put("jobb", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobC", Status.KILLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobc", Status.KILLED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ expectedStateMap.put("jobd:innerJobA", Status.KILLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+
+ Assert.assertEquals(Status.KILLED, flow.getStatus());
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests that pause and resume work
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testPause() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow flow = runner.getExecutableFlow();
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.pause("test");
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ // 2.1 JOB A COMPLETES SUCCESSFULLY AFTER PAUSE
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(flow.getStatus(), Status.PAUSED);
+
+ // 2.2 Flow is unpaused
+ runner.resume("test");
+ pause(250);
+ Assert.assertEquals(flow.getStatus(), Status.RUNNING);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 3. jobb:Inner completes
+ runner.pause("test");
+
+ /// innerJobA completes, but paused
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob(
+ Props.of("output.jobb.innerJobA", "jobb.innerJobA"));
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.resume("test");
+ pause(250);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ /// innerJobB, C completes
+ InteractiveTestJob.getTestJob("jobb:innerJobB").succeedJob(
+ Props.of("output.jobb.innerJobB", "jobb.innerJobB"));
+ InteractiveTestJob.getTestJob("jobb:innerJobC").succeedJob(
+ Props.of("output.jobb.innerJobC", "jobb.innerJobC"));
+ pause(250);
+ expectedStateMap.put("jobb:innerJobB", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerFlow", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 4. Finish up on inner flow for jobb
+ InteractiveTestJob.getTestJob("jobb:innerFlow").succeedJob(
+ Props.of("output1.jobb", "test1", "output2.jobb", "test2"));
+ pause(250);
+ expectedStateMap.put("jobb:innerFlow", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 5. Finish jobc, jobd
+ InteractiveTestJob.getTestJob("jobc").succeedJob(
+ Props.of("output.jobc", "jobc"));
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("jobd:innerFlow2").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerFlow2", Status.SUCCEEDED);
+ expectedStateMap.put("jobd", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 6. Finish off flow
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("jobe").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.SUCCEEDED);
+ expectedStateMap.put("jobf", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ InteractiveTestJob.getTestJob("jobf").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobf", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.SUCCEEDED, flow.getStatus());
+
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Test the condition for a manual invocation of a KILL (cancel) on a flow
+ * that has been paused. The flow should unpause and be killed immediately.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testPauseKill() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf");
+
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow flow = runner.getExecutableFlow();
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.pause("me");
+ pause(250);
+ Assert.assertEquals(flow.getStatus(), Status.PAUSED);
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerJobA").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ expectedStateMap.put("jobd:innerJobA", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.kill("me");
+ pause(250);
+ expectedStateMap.put("joba1", Status.KILLED);
+ expectedStateMap.put("jobb:innerJobB", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerJobC", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobb", Status.KILLED);
+ expectedStateMap.put("jobc", Status.KILLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobd", Status.KILLED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.KILLED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests the case where a failure occurs on a Paused flow. In this case, the
+ * flow should stay paused.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testPauseFail() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf",
+ FailureAction.FINISH_CURRENTLY_RUNNING);
+
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow flow = runner.getExecutableFlow();
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.pause("me");
+ pause(250);
+ Assert.assertEquals(flow.getStatus(), Status.PAUSED);
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerJobA").failJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerJobA", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(flow.getStatus(), Status.PAUSED);
+
+ runner.resume("me");
+ pause(250);
+ expectedStateMap.put("jobb:innerJobB", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerJobC", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobb", Status.KILLED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobd", Status.FAILED);
+
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Test the condition when a Finish all possible is called during a pause.
+ * The Failure is not acted upon until the flow is resumed.
+ *
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testPauseFailFinishAll() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf",
+ FailureAction.FINISH_ALL_POSSIBLE);
+
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow flow = runner.getExecutableFlow();
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(250);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.pause("me");
+ pause(250);
+ Assert.assertEquals(flow.getStatus(), Status.PAUSED);
+ InteractiveTestJob.getTestJob("jobb:innerJobA").succeedJob();
+ InteractiveTestJob.getTestJob("jobd:innerJobA").failJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerJobA", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobA", Status.SUCCEEDED);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.resume("me");
+ pause(250);
+ expectedStateMap.put("jobb:innerJobB", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobC", Status.RUNNING);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobd", Status.FAILED);
+
+ InteractiveTestJob.getTestJob("jobc").succeedJob();
+ InteractiveTestJob.getTestJob("joba1").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobB").succeedJob();
+ InteractiveTestJob.getTestJob("jobb:innerJobC").succeedJob();
+ pause(250);
+ InteractiveTestJob.getTestJob("jobb:innerFlow").succeedJob();
+ pause(250);
+ expectedStateMap.put("jobc", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobB", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerJobC", Status.SUCCEEDED);
+ expectedStateMap.put("jobb:innerFlow", Status.SUCCEEDED);
+ expectedStateMap.put("jobb", Status.SUCCEEDED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+
+ compareStates(expectedStateMap, nodeMap);
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+ /**
+ * Tests the case when a flow is paused and a failure causes a kill. The
+ * flow should die immediately regardless of the 'paused' status.
+ * @throws Exception
+ */
+ @Ignore @Test
+ public void testPauseFailKill() throws Exception {
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ FlowRunner runner = createFlowRunner(eventCollector, "jobf",
+ FailureAction.CANCEL_ALL);
+
+ Map<String, Status> expectedStateMap = new HashMap<String, Status>();
+ Map<String, ExecutableNode> nodeMap = new HashMap<String, ExecutableNode>();
+
+ // 1. START FLOW
+ ExecutableFlow flow = runner.getExecutableFlow();
+ createExpectedStateMap(flow, expectedStateMap, nodeMap);
+ Thread thread = runFlowRunnerInThread(runner);
+ pause(250);
+ // After it starts up, only joba should be running
+ expectedStateMap.put("joba", Status.RUNNING);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ // 2. JOB A COMPLETES SUCCESSFULLY
+ InteractiveTestJob.getTestJob("joba").succeedJob();
+ pause(500);
+ expectedStateMap.put("joba", Status.SUCCEEDED);
+ expectedStateMap.put("joba1", Status.RUNNING);
+ expectedStateMap.put("jobb", Status.RUNNING);
+ expectedStateMap.put("jobc", Status.RUNNING);
+ expectedStateMap.put("jobd", Status.RUNNING);
+ expectedStateMap.put("jobd:innerJobA", Status.RUNNING);
+ expectedStateMap.put("jobb:innerJobA", Status.RUNNING);
+ compareStates(expectedStateMap, nodeMap);
+
+ runner.pause("me");
+ pause(250);
+ Assert.assertEquals(flow.getStatus(), Status.PAUSED);
+ InteractiveTestJob.getTestJob("jobd:innerJobA").failJob();
+ pause(250);
+ expectedStateMap.put("jobd:innerJobA", Status.FAILED);
+ expectedStateMap.put("jobd:innerFlow2", Status.CANCELLED);
+ expectedStateMap.put("jobd", Status.FAILED);
+ expectedStateMap.put("jobb:innerJobA", Status.KILLED);
+ expectedStateMap.put("jobb:innerJobB", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerJobC", Status.CANCELLED);
+ expectedStateMap.put("jobb:innerFlow", Status.CANCELLED);
+ expectedStateMap.put("jobb", Status.KILLED);
+ expectedStateMap.put("jobc", Status.KILLED);
+ expectedStateMap.put("jobe", Status.CANCELLED);
+ expectedStateMap.put("jobf", Status.CANCELLED);
+ expectedStateMap.put("joba1", Status.KILLED);
+ compareStates(expectedStateMap, nodeMap);
+
+ Assert.assertEquals(Status.FAILED, flow.getStatus());
+ Assert.assertFalse(thread.isAlive());
+ }
+
+
+ private Thread runFlowRunnerInThread(FlowRunner runner) {
+ Thread thread = new Thread(runner);
+ thread.start();
+ return thread;
+ }
+
+ private void pause(long millisec) {
+ synchronized(this) {
+ try {
+ wait(millisec);
+ }
+ catch (InterruptedException e) {
+ }
+ }
+ }
+
+ private void createExpectedStateMap(ExecutableFlowBase flow,
+ Map<String, Status> expectedStateMap,
+ Map<String, ExecutableNode> nodeMap) {
+ for (ExecutableNode node: flow.getExecutableNodes()) {
+ expectedStateMap.put(node.getNestedId(), node.getStatus());
+ nodeMap.put(node.getNestedId(), node);
+ if (node instanceof ExecutableFlowBase) {
+ createExpectedStateMap((ExecutableFlowBase)node, expectedStateMap,
+ nodeMap);
+ }
+ }
+ }
+
+ private void compareStates(Map<String, Status> expectedStateMap,
+ Map<String, ExecutableNode> nodeMap) {
+ for (String printedId: expectedStateMap.keySet()) {
+ Status expectedStatus = expectedStateMap.get(printedId);
+ ExecutableNode node = nodeMap.get(printedId);
+
+ if (expectedStatus != node.getStatus()) {
+ Assert.fail("Expected values do not match for " + printedId
+ + ". Expected " + expectedStatus + ", instead received "
+ + node.getStatus());
+ }
+ }
+ }
+
+ private void prepareProject(File directory)
+ throws ProjectManagerException, IOException {
+ DirectoryFlowLoader loader = new DirectoryFlowLoader(logger);
+ loader.loadProjectFlow(directory);
+ if (!loader.getErrors().isEmpty()) {
+ for (String error: loader.getErrors()) {
+ System.out.println(error);
+ }
+
+ throw new RuntimeException("Errors found in setup");
+ }
+
+ flowMap = loader.getFlowMap();
+ project.setFlows(flowMap);
+ FileUtils.copyDirectory(directory, workingDir);
+ }
+
+ private FlowRunner createFlowRunner(EventCollectorListener eventCollector,
+ String flowName) throws Exception {
+ return createFlowRunner(eventCollector, flowName,
+ FailureAction.FINISH_CURRENTLY_RUNNING);
+ }
+
+ private FlowRunner createFlowRunner(EventCollectorListener eventCollector,
+ String flowName, FailureAction action) throws Exception {
+ Flow flow = flowMap.get(flowName);
+
+ int exId = id++;
+ ExecutableFlow exFlow = new ExecutableFlow(project, flow);
+ exFlow.setExecutionPath(workingDir.getPath());
+ exFlow.setExecutionId(exId);
+
+ Map<String, String> flowParam = new HashMap<String, String>();
+ flowParam.put("param4", "override.4");
+ flowParam.put("param10", "override.10");
+ flowParam.put("param11", "override.11");
+ exFlow.getExecutionOptions().addAllFlowParameters(flowParam);
+ exFlow.getExecutionOptions().setFailureAction(action);
+ fakeExecutorLoader.uploadExecutableFlow(exFlow);
+
+ FlowRunner runner = new FlowRunner(
+ fakeExecutorLoader.fetchExecutableFlow(exId), fakeExecutorLoader,
+ fakeProjectLoader, jobtypeManager);
+
+ runner.addListener(eventCollector);
+
+ return runner;
+ }
+
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/JobRunnerTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/JobRunnerTest.java
new file mode 100644
index 0000000..8f439f3
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/JobRunnerTest.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import azkaban.event.Event;
+import azkaban.event.Event.Type;
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutableNode;
+import azkaban.executor.ExecutorLoader;
+import azkaban.executor.JavaJob;
+import azkaban.executor.MockExecutorLoader;
+import azkaban.executor.SleepJavaJob;
+import azkaban.executor.Status;
+import azkaban.jobExecutor.ProcessJob;
+import azkaban.jobtype.JobTypeManager;
+import azkaban.project.MockProjectLoader;
+import azkaban.utils.Props;
+
+public class JobRunnerTest {
+ private File workingDir;
+ private JobTypeManager jobtypeManager;
+ private Logger logger = Logger.getLogger("JobRunnerTest");
+
+ public JobRunnerTest() {
+
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ System.out.println("Create temp dir");
+ workingDir = new File("_AzkabanTestDir_" + System.currentTimeMillis());
+ if (workingDir.exists()) {
+ FileUtils.deleteDirectory(workingDir);
+ }
+ workingDir.mkdirs();
+ jobtypeManager =
+ new JobTypeManager(null, null, this.getClass().getClassLoader());
+
+ jobtypeManager.getJobTypePluginSet().addPluginClass("java", JavaJob.class);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ System.out.println("Teardown temp dir");
+ if (workingDir != null) {
+ FileUtils.deleteDirectory(workingDir);
+ workingDir = null;
+ }
+ }
+
+ @Ignore @Test
+ public void testBasicRun() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(1, "testJob", 1, false, loader, eventCollector);
+ ExecutableNode node = runner.getNode();
+
+ eventCollector.handleEvent(Event.create(null, Event.Type.JOB_STARTED));
+ Assert.assertTrue(runner.getStatus() != Status.SUCCEEDED
+ || runner.getStatus() != Status.FAILED);
+
+ runner.run();
+ eventCollector.handleEvent(Event.create(null, Event.Type.JOB_FINISHED));
+
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue("Node status is " + node.getStatus(),
+ node.getStatus() == Status.SUCCEEDED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() > 1000);
+
+ File logFile = new File(runner.getLogFilePath());
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps != null);
+ Assert.assertTrue(logFile.exists());
+
+ Assert.assertTrue(loader.getNodeUpdateCount(node.getId()) == 3);
+
+ Assert.assertTrue(eventCollector.checkOrdering());
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_STARTED,
+ Type.JOB_STATUS_CHANGED, Type.JOB_FINISHED });
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void testFailedRun() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(1, "testJob", 1, true, loader, eventCollector);
+ ExecutableNode node = runner.getNode();
+
+ Assert.assertTrue(runner.getStatus() != Status.SUCCEEDED
+ || runner.getStatus() != Status.FAILED);
+ runner.run();
+
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue(node.getStatus() == Status.FAILED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() > 1000);
+
+ File logFile = new File(runner.getLogFilePath());
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps == null);
+ Assert.assertTrue(logFile.exists());
+ Assert.assertTrue(eventCollector.checkOrdering());
+ Assert.assertTrue(!runner.isKilled());
+ Assert.assertTrue(loader.getNodeUpdateCount(node.getId()) == 3);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_STARTED,
+ Type.JOB_STATUS_CHANGED, Type.JOB_FINISHED });
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDisabledRun() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(1, "testJob", 1, false, loader, eventCollector);
+ ExecutableNode node = runner.getNode();
+
+ node.setStatus(Status.DISABLED);
+
+ // Should be disabled.
+ Assert.assertTrue(runner.getStatus() == Status.DISABLED);
+ runner.run();
+
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue(node.getStatus() == Status.SKIPPED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ // Give it 10 ms to fail.
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() < 10);
+
+ // Log file and output files should not exist.
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps == null);
+ Assert.assertTrue(runner.getLogFilePath() == null);
+ Assert.assertTrue(eventCollector.checkOrdering());
+
+ Assert.assertTrue(loader.getNodeUpdateCount(node.getId()) == null);
+
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_STARTED,
+ Type.JOB_FINISHED });
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test
+ public void testPreKilledRun() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(1, "testJob", 1, false, loader, eventCollector);
+ ExecutableNode node = runner.getNode();
+
+ node.setStatus(Status.KILLED);
+
+ // Should be killed.
+ Assert.assertTrue(runner.getStatus() == Status.KILLED);
+ runner.run();
+
+ // Should just skip the run and not change
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue(node.getStatus() == Status.KILLED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ // Give it 10 ms to fail.
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() < 10);
+
+ Assert.assertTrue(loader.getNodeUpdateCount(node.getId()) == null);
+
+ // Log file and output files should not exist.
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps == null);
+ Assert.assertTrue(runner.getLogFilePath() == null);
+ Assert.assertTrue(!runner.isKilled());
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_STARTED,
+ Type.JOB_FINISHED });
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void testCancelRun() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(13, "testJob", 10, false, loader, eventCollector);
+ ExecutableNode node = runner.getNode();
+
+ Assert.assertTrue(runner.getStatus() != Status.SUCCEEDED
+ || runner.getStatus() != Status.FAILED);
+
+ Thread thread = new Thread(runner);
+ thread.start();
+
+ synchronized (this) {
+ try {
+ wait(2000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ runner.kill();
+ try {
+ wait(500);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue("Status is " + node.getStatus(),
+ node.getStatus() == Status.KILLED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ // Give it 10 ms to fail.
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() < 3000);
+ Assert.assertTrue(loader.getNodeUpdateCount(node.getId()) == 3);
+
+ // Log file and output files should not exist.
+ File logFile = new File(runner.getLogFilePath());
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps == null);
+ Assert.assertTrue(logFile.exists());
+ Assert.assertTrue(eventCollector.checkOrdering());
+ Assert.assertTrue(runner.isKilled());
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_STARTED,
+ Type.JOB_STATUS_CHANGED, Type.JOB_FINISHED });
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Ignore @Test
+ public void testDelayedExecutionJob() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(1, "testJob", 1, false, loader, eventCollector);
+ runner.setDelayStart(5000);
+ long startTime = System.currentTimeMillis();
+ ExecutableNode node = runner.getNode();
+
+ eventCollector.handleEvent(Event.create(null, Event.Type.JOB_STARTED));
+ Assert.assertTrue(runner.getStatus() != Status.SUCCEEDED
+ || runner.getStatus() != Status.FAILED);
+
+ runner.run();
+ eventCollector.handleEvent(Event.create(null, Event.Type.JOB_FINISHED));
+
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue("Node status is " + node.getStatus(),
+ node.getStatus() == Status.SUCCEEDED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() > 1000);
+ Assert.assertTrue(node.getStartTime() - startTime >= 5000);
+
+ File logFile = new File(runner.getLogFilePath());
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps != null);
+ Assert.assertTrue(logFile.exists());
+ Assert.assertFalse(runner.isKilled());
+ Assert.assertTrue(loader.getNodeUpdateCount(node.getId()) == 3);
+
+ Assert.assertTrue(eventCollector.checkOrdering());
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_STARTED,
+ Type.JOB_STATUS_CHANGED, Type.JOB_FINISHED });
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDelayedExecutionCancelledJob() {
+ MockExecutorLoader loader = new MockExecutorLoader();
+ EventCollectorListener eventCollector = new EventCollectorListener();
+ JobRunner runner =
+ createJobRunner(1, "testJob", 1, false, loader, eventCollector);
+ runner.setDelayStart(5000);
+ long startTime = System.currentTimeMillis();
+ ExecutableNode node = runner.getNode();
+
+ eventCollector.handleEvent(Event.create(null, Event.Type.JOB_STARTED));
+ Assert.assertTrue(runner.getStatus() != Status.SUCCEEDED
+ || runner.getStatus() != Status.FAILED);
+
+ Thread thread = new Thread(runner);
+ thread.start();
+
+ synchronized (this) {
+ try {
+ wait(2000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ runner.kill();
+ try {
+ wait(500);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ eventCollector.handleEvent(Event.create(null, Event.Type.JOB_FINISHED));
+
+ Assert.assertTrue(runner.getStatus() == node.getStatus());
+ Assert.assertTrue("Node status is " + node.getStatus(),
+ node.getStatus() == Status.KILLED);
+ Assert.assertTrue(node.getStartTime() > 0 && node.getEndTime() > 0);
+ Assert.assertTrue(node.getEndTime() - node.getStartTime() < 1000);
+ Assert.assertTrue(node.getStartTime() - startTime >= 2000);
+ Assert.assertTrue(node.getStartTime() - startTime <= 5000);
+ Assert.assertTrue(runner.isKilled());
+
+ File logFile = new File(runner.getLogFilePath());
+ Props outputProps = runner.getNode().getOutputProps();
+ Assert.assertTrue(outputProps == null);
+ Assert.assertTrue(logFile.exists());
+
+ Assert.assertTrue(eventCollector.checkOrdering());
+ try {
+ eventCollector.checkEventExists(new Type[] { Type.JOB_FINISHED });
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ private Props createProps(int sleepSec, boolean fail) {
+ Props props = new Props();
+ props.put("type", "java");
+
+ props.put(JavaJob.JOB_CLASS, SleepJavaJob.class.getName());
+ props.put("seconds", sleepSec);
+ props.put(ProcessJob.WORKING_DIR, workingDir.getPath());
+ props.put("fail", String.valueOf(fail));
+
+ return props;
+ }
+
+ private JobRunner createJobRunner(int execId, String name, int time,
+ boolean fail, ExecutorLoader loader, EventCollectorListener listener) {
+ ExecutableFlow flow = new ExecutableFlow();
+ flow.setExecutionId(execId);
+ ExecutableNode node = new ExecutableNode();
+ node.setId(name);
+ node.setParentFlow(flow);
+
+ Props props = createProps(time, fail);
+ node.setInputProps(props);
+ HashSet<String> proxyUsers = new HashSet<String>();
+ proxyUsers.add(flow.getSubmitUser());
+ JobRunner runner = new JobRunner(node, workingDir, loader, jobtypeManager);
+ runner.setLogSettings(logger, "5MB", 4);
+
+ runner.addListener(listener);
+ return runner;
+ }
+
+}
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/ProjectVersionsTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/ProjectVersionsTest.java
new file mode 100644
index 0000000..221ba31
--- /dev/null
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/ProjectVersionsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2014 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.execapp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ProjectVersionsTest {
+
+ @Test
+ public void testVersionOrdering() {
+ ArrayList<ProjectVersion> pversion = new ArrayList<ProjectVersion>();
+ pversion.add(new ProjectVersion(1, 2));
+ pversion.add(new ProjectVersion(1, 3));
+ pversion.add(new ProjectVersion(1, 1));
+
+ Collections.sort(pversion);
+
+ int i = 0;
+ for (ProjectVersion version : pversion) {
+ Assert.assertTrue(i < version.getVersion());
+ i = version.getVersion();
+ }
+ }
+}
diff --git a/azkaban-migration/src/main/resources/log4j.properties b/azkaban-migration/src/main/resources/log4j.properties
new file mode 100644
index 0000000..52008f9
--- /dev/null
+++ b/azkaban-migration/src/main/resources/log4j.properties
@@ -0,0 +1,29 @@
+log4j.rootLogger=INFO, Console
+log4j.logger.azkaban.webapp=INFO, WebServer
+log4j.logger.azkaban.webapp.servlet.AbstractAzkabanServlet=INFO, R
+log4j.logger.azkaban.webapp.servlet.LoginAbstractAzkabanServlet=INFO, R
+
+log4j.appender.R=org.apache.log4j.RollingFileAppender
+log4j.appender.R.layout=org.apache.log4j.PatternLayout
+log4j.appender.R.File=azkaban-access.log
+log4j.appender.R.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
+log4j.appender.R.MaxFileSize=102400MB
+log4j.appender.R.MaxBackupIndex=2
+
+log4j.appender.WebServer=org.apache.log4j.RollingFileAppender
+log4j.appender.WebServer.layout=org.apache.log4j.PatternLayout
+log4j.appender.WebServer.File=azkaban-webserver.log
+log4j.appender.WebServer.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
+log4j.appender.WebServer.MaxFileSize=102400MB
+log4j.appender.WebServer.MaxBackupIndex=2
+
+log4j.appender.ExecServer=org.apache.log4j.RollingFileAppender
+log4j.appender.ExecServer.layout=org.apache.log4j.PatternLayout
+log4j.appender.ExecServer.File=azkaban-execserver.log
+log4j.appender.ExecServer.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
+log4j.appender.ExecServer.MaxFileSize=102400MB
+log4j.appender.ExecServer.MaxBackupIndex=2
+
+log4j.appender.Console=org.apache.log4j.ConsoleAppender
+log4j.appender.Console.layout=org.apache.log4j.PatternLayout
+log4j.appender.Console.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
\ No newline at end of file
diff --git a/azkaban-migration/src/package/bin/schedule2trigger.sh b/azkaban-migration/src/package/bin/schedule2trigger.sh
new file mode 100755
index 0000000..1178cf3
--- /dev/null
+++ b/azkaban-migration/src/package/bin/schedule2trigger.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+java -cp "lib/*:extlib/*" azkaban.migration.schedule2trigger.Schedule2Trigger conf/azkaban.properties
+
diff --git a/azkaban-soloserver/src/main/resources/log4j.properties b/azkaban-soloserver/src/main/resources/log4j.properties
new file mode 100644
index 0000000..b69d468
--- /dev/null
+++ b/azkaban-soloserver/src/main/resources/log4j.properties
@@ -0,0 +1,14 @@
+log4j.rootLogger=INFO, Console
+log4j.logger.azkaban.webapp=INFO, WebServer
+log4j.logger.azkaban.soloserver=INFO, WebServer
+
+log4j.appender.WebServer=org.apache.log4j.RollingFileAppender
+log4j.appender.WebServer.layout=org.apache.log4j.PatternLayout
+log4j.appender.WebServer.File=azkaban-webserver.log
+log4j.appender.WebServer.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
+log4j.appender.WebServer.MaxFileSize=102400MB
+log4j.appender.WebServer.MaxBackupIndex=2
+
+log4j.appender.Console=org.apache.log4j.ConsoleAppender
+log4j.appender.Console.layout=org.apache.log4j.PatternLayout
+log4j.appender.Console.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS Z} %p [%c{1}] [Azkaban] %m%n
diff --git a/azkaban-soloserver/src/package/conf/azkaban.private.properties b/azkaban-soloserver/src/package/conf/azkaban.private.properties
new file mode 100644
index 0000000..cce1792
--- /dev/null
+++ b/azkaban-soloserver/src/package/conf/azkaban.private.properties
@@ -0,0 +1 @@
+# Optional Properties that are hidden to the executions
\ No newline at end of file
diff --git a/azkaban-soloserver/src/package/conf/global.properties b/azkaban-soloserver/src/package/conf/global.properties
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/azkaban-soloserver/src/package/conf/global.properties
diff --git a/azkaban-sql/src/sql/create.execution_flows.sql b/azkaban-sql/src/sql/create.execution_flows.sql
new file mode 100644
index 0000000..b2f7625
--- /dev/null
+++ b/azkaban-sql/src/sql/create.execution_flows.sql
@@ -0,0 +1,20 @@
+CREATE TABLE execution_flows (
+ exec_id INT NOT NULL AUTO_INCREMENT,
+ project_id INT NOT NULL,
+ version INT NOT NULL,
+ flow_id VARCHAR(128) NOT NULL,
+ status TINYINT,
+ submit_user VARCHAR(64),
+ submit_time BIGINT,
+ update_time BIGINT,
+ start_time BIGINT,
+ end_time BIGINT,
+ enc_type TINYINT,
+ flow_data LONGBLOB,
+ PRIMARY KEY (exec_id)
+);
+
+CREATE INDEX ex_flows_start_time ON execution_flows(start_time);
+CREATE INDEX ex_flows_end_time ON execution_flows(end_time);
+CREATE INDEX ex_flows_time_range ON execution_flows(start_time, end_time);
+CREATE INDEX ex_flows_flows ON execution_flows(project_id, flow_id);
diff --git a/azkaban-sql/src/sql/create.execution_jobs.sql b/azkaban-sql/src/sql/create.execution_jobs.sql
new file mode 100644
index 0000000..a62d3a9
--- /dev/null
+++ b/azkaban-sql/src/sql/create.execution_jobs.sql
@@ -0,0 +1,19 @@
+CREATE TABLE execution_jobs (
+ exec_id INT NOT NULL,
+ project_id INT NOT NULL,
+ version INT NOT NULL,
+ flow_id VARCHAR(128) NOT NULL,
+ job_id VARCHAR(128) NOT NULL,
+ attempt INT,
+ start_time BIGINT,
+ end_time BIGINT,
+ status TINYINT,
+ input_params LONGBLOB,
+ output_params LONGBLOB,
+ attachments LONGBLOB,
+ PRIMARY KEY (exec_id, job_id, attempt)
+);
+
+CREATE INDEX exec_job ON execution_jobs(exec_id, job_id);
+CREATE INDEX exec_id ON execution_jobs(exec_id);
+CREATE INDEX ex_job_id ON execution_jobs(project_id, job_id);
diff --git a/azkaban-sql/src/sql/create.execution_logs.sql b/azkaban-sql/src/sql/create.execution_logs.sql
new file mode 100644
index 0000000..0aa6a36
--- /dev/null
+++ b/azkaban-sql/src/sql/create.execution_logs.sql
@@ -0,0 +1,14 @@
+CREATE TABLE execution_logs (
+ exec_id INT NOT NULL,
+ name VARCHAR(128),
+ attempt INT,
+ enc_type TINYINT,
+ start_byte INT,
+ end_byte INT,
+ log LONGBLOB,
+ upload_time BIGINT,
+ PRIMARY KEY (exec_id, name, attempt, start_byte)
+);
+
+CREATE INDEX ex_log_attempt ON execution_logs(exec_id, name, attempt);
+CREATE INDEX ex_log_index ON execution_logs(exec_id, name);
\ No newline at end of file
diff --git a/azkaban-sql/src/sql/create.project_events.sql b/azkaban-sql/src/sql/create.project_events.sql
new file mode 100644
index 0000000..dd24d5f
--- /dev/null
+++ b/azkaban-sql/src/sql/create.project_events.sql
@@ -0,0 +1,9 @@
+CREATE TABLE project_events (
+ project_id INT NOT NULL,
+ event_type TINYINT NOT NULL,
+ event_time BIGINT NOT NULL,
+ username VARCHAR(64),
+ message VARCHAR(512)
+);
+
+CREATE INDEX log ON project_events(project_id, event_time);
diff --git a/azkaban-sql/src/sql/create.properties.sql b/azkaban-sql/src/sql/create.properties.sql
new file mode 100644
index 0000000..aaa37ec
--- /dev/null
+++ b/azkaban-sql/src/sql/create.properties.sql
@@ -0,0 +1,7 @@
+CREATE TABLE properties (
+ name VARCHAR(64) NOT NULL,
+ type INT NOT NULL,
+ modified_time BIGINT NOT NULL,
+ value VARCHAR(256),
+ PRIMARY KEY (name, type)
+);
\ No newline at end of file
diff --git a/azkaban-sql/src/sql/database.properties b/azkaban-sql/src/sql/database.properties
new file mode 100644
index 0000000..b68be28
--- /dev/null
+++ b/azkaban-sql/src/sql/database.properties
@@ -0,0 +1 @@
+version=
diff --git a/azkaban-sql/src/sql/update.project_properties.2.1.sql b/azkaban-sql/src/sql/update.project_properties.2.1.sql
new file mode 100644
index 0000000..32d0821
--- /dev/null
+++ b/azkaban-sql/src/sql/update.project_properties.2.1.sql
@@ -0,0 +1,3 @@
+ALTER TABLE project_properties MODIFY name VARCHAR(255);
+
+
diff --git a/azkaban-test/src/test/resources/executions/animal/albatross.job b/azkaban-test/src/test/resources/executions/animal/albatross.job
new file mode 100644
index 0000000..4f9cf95
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/albatross.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=21
+fail=false
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/animals.job b/azkaban-test/src/test/resources/executions/animal/animals.job
new file mode 100644
index 0000000..8d2e4a4
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/animals.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=11
+fail=false
+dependencies=humpback-whale
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/baboon.job b/azkaban-test/src/test/resources/executions/animal/baboon.job
new file mode 100644
index 0000000..0a4e652
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/baboon.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=7
+fail=false
+dependencies=albatross
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/caiman.job b/azkaban-test/src/test/resources/executions/animal/caiman.job
new file mode 100644
index 0000000..2e258ac
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/caiman.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=13
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/camel.job b/azkaban-test/src/test/resources/executions/animal/camel.job
new file mode 100644
index 0000000..a854a97
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/camel.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=18
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/capybara.job b/azkaban-test/src/test/resources/executions/animal/capybara.job
new file mode 100644
index 0000000..02db6f5
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/capybara.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=16
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/cat.job b/azkaban-test/src/test/resources/executions/animal/cat.job
new file mode 100644
index 0000000..f7de0a7
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/cat.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=23
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/caterpillar.job b/azkaban-test/src/test/resources/executions/animal/caterpillar.job
new file mode 100644
index 0000000..6b4347c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/caterpillar.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=19
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/catfish.job b/azkaban-test/src/test/resources/executions/animal/catfish.job
new file mode 100644
index 0000000..538d93a
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/catfish.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=14
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/centipede.job b/azkaban-test/src/test/resources/executions/animal/centipede.job
new file mode 100644
index 0000000..2e258ac
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/centipede.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=13
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/chameleon.job b/azkaban-test/src/test/resources/executions/animal/chameleon.job
new file mode 100644
index 0000000..7393129
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/chameleon.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=25
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/cheetah.job b/azkaban-test/src/test/resources/executions/animal/cheetah.job
new file mode 100644
index 0000000..5003eb3
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/cheetah.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=7
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/chicken.job b/azkaban-test/src/test/resources/executions/animal/chicken.job
new file mode 100644
index 0000000..dbbbc72
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/chicken.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=29
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/chihuahua.job b/azkaban-test/src/test/resources/executions/animal/chihuahua.job
new file mode 100644
index 0000000..a854a97
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/chihuahua.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=18
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/clown-fish.job b/azkaban-test/src/test/resources/executions/animal/clown-fish.job
new file mode 100644
index 0000000..538d93a
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/clown-fish.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=14
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/cockroach.job b/azkaban-test/src/test/resources/executions/animal/cockroach.job
new file mode 100644
index 0000000..7449537
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/cockroach.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=9
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/cougar.job b/azkaban-test/src/test/resources/executions/animal/cougar.job
new file mode 100644
index 0000000..d29ca57
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/cougar.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=30
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/cuttlefish.job b/azkaban-test/src/test/resources/executions/animal/cuttlefish.job
new file mode 100644
index 0000000..dbbbc72
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/cuttlefish.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=29
+fail=false
+dependencies=baboon
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/flamingo.job b/azkaban-test/src/test/resources/executions/animal/flamingo.job
new file mode 100644
index 0000000..07b68a9
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/flamingo.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=7
+fail=false
+dependencies=camel,elephant
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/gorilla.job b/azkaban-test/src/test/resources/executions/animal/gorilla.job
new file mode 100644
index 0000000..f5b16e7
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/gorilla.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=27
+fail=false
+dependencies=flamingo
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/animal/humpback-whale.job b/azkaban-test/src/test/resources/executions/animal/humpback-whale.job
new file mode 100644
index 0000000..4981525
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/animal/humpback-whale.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=19
+fail=false
+dependencies=gorilla
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded/innerFlow.job b/azkaban-test/src/test/resources/executions/embedded/innerFlow.job
new file mode 100644
index 0000000..da71d64
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/innerFlow.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobB,innerJobC
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded/innerJobA.job b/azkaban-test/src/test/resources/executions/embedded/innerJobA.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/innerJobA.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/embedded/innerJobB.job b/azkaban-test/src/test/resources/executions/embedded/innerJobB.job
new file mode 100644
index 0000000..178bbef
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/innerJobB.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embedded/innerJobC.job b/azkaban-test/src/test/resources/executions/embedded/innerJobC.job
new file mode 100644
index 0000000..178bbef
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/innerJobC.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embedded/joba.job b/azkaban-test/src/test/resources/executions/embedded/joba.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/joba.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/embedded/jobb.job b/azkaban-test/src/test/resources/executions/embedded/jobb.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/jobb.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded/jobc.job b/azkaban-test/src/test/resources/executions/embedded/jobc.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/jobc.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded/jobd.job b/azkaban-test/src/test/resources/executions/embedded/jobd.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/jobd.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded/jobe.job b/azkaban-test/src/test/resources/executions/embedded/jobe.job
new file mode 100644
index 0000000..fe986d5
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded/jobe.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=jobb,jobc,jobd
diff --git a/azkaban-test/src/test/resources/executions/embedded2/innerFlow.job b/azkaban-test/src/test/resources/executions/embedded2/innerFlow.job
new file mode 100644
index 0000000..dfa0e9d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/innerFlow.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=innerJobB,innerJobC
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/innerFlow2.job b/azkaban-test/src/test/resources/executions/embedded2/innerFlow2.job
new file mode 100644
index 0000000..35cbccb
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/innerFlow2.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=innerJobA
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/innerJobA.job b/azkaban-test/src/test/resources/executions/embedded2/innerJobA.job
new file mode 100644
index 0000000..35ebd72
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/innerJobA.job
@@ -0,0 +1,2 @@
+type=test
+
diff --git a/azkaban-test/src/test/resources/executions/embedded2/innerJobB.job b/azkaban-test/src/test/resources/executions/embedded2/innerJobB.job
new file mode 100644
index 0000000..dca1223
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/innerJobB.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embedded2/innerJobC.job b/azkaban-test/src/test/resources/executions/embedded2/innerJobC.job
new file mode 100644
index 0000000..dca1223
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/innerJobC.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embedded2/joba.job b/azkaban-test/src/test/resources/executions/embedded2/joba.job
new file mode 100644
index 0000000..80ad69e
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/joba.job
@@ -0,0 +1,2 @@
+type=test
+param1=joba.1
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/joba1.job b/azkaban-test/src/test/resources/executions/embedded2/joba1.job
new file mode 100644
index 0000000..98fd5f5
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/joba1.job
@@ -0,0 +1 @@
+type=test
diff --git a/azkaban-test/src/test/resources/executions/embedded2/jobb.job b/azkaban-test/src/test/resources/executions/embedded2/jobb.job
new file mode 100644
index 0000000..4531028
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/jobb.job
@@ -0,0 +1,5 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
+testprops=moo
+output.override=jobb
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/jobc.job b/azkaban-test/src/test/resources/executions/embedded2/jobc.job
new file mode 100644
index 0000000..2bfc5ff
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/jobc.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded2/jobd.job b/azkaban-test/src/test/resources/executions/embedded2/jobd.job
new file mode 100644
index 0000000..e80f82b
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/jobd.job
@@ -0,0 +1,4 @@
+type=flow
+flow.name=innerFlow2
+dependencies=joba
+jobdprop=poop
diff --git a/azkaban-test/src/test/resources/executions/embedded2/jobe.job b/azkaban-test/src/test/resources/executions/embedded2/jobe.job
new file mode 100644
index 0000000..331a81e
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/jobe.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=jobb,jobc,jobd
diff --git a/azkaban-test/src/test/resources/executions/embedded2/jobf.job b/azkaban-test/src/test/resources/executions/embedded2/jobf.job
new file mode 100644
index 0000000..b1b00ce
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/jobf.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=jobe,joba1
diff --git a/azkaban-test/src/test/resources/executions/embedded2/jobg.job b/azkaban-test/src/test/resources/executions/embedded2/jobg.job
new file mode 100644
index 0000000..b1b00ce
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/jobg.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=jobe,joba1
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipeline1.job b/azkaban-test/src/test/resources/executions/embedded2/pipeline1.job
new file mode 100644
index 0000000..4afbfdc
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipeline1.job
@@ -0,0 +1 @@
+type=test
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipeline1_1.job b/azkaban-test/src/test/resources/executions/embedded2/pipeline1_1.job
new file mode 100644
index 0000000..cfe35cc
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipeline1_1.job
@@ -0,0 +1,4 @@
+type=flow
+flow.name=innerFlow2
+testprops=moo
+output.override=jobb
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipeline1_2.job b/azkaban-test/src/test/resources/executions/embedded2/pipeline1_2.job
new file mode 100644
index 0000000..711d823
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipeline1_2.job
@@ -0,0 +1,5 @@
+type=flow
+flow.name=innerFlow2
+dependencies=pipeline1_1
+testprops=moo
+output.override=jobb
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipeline2.job b/azkaban-test/src/test/resources/executions/embedded2/pipeline2.job
new file mode 100644
index 0000000..84f6498
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipeline2.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=pipeline1
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipeline4.job b/azkaban-test/src/test/resources/executions/embedded2/pipeline4.job
new file mode 100644
index 0000000..b24c4ba
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipeline4.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=pipelineEmbeddedFlow3
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipelineEmbeddedFlow3.job b/azkaban-test/src/test/resources/executions/embedded2/pipelineEmbeddedFlow3.job
new file mode 100644
index 0000000..0a1ae46
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipelineEmbeddedFlow3.job
@@ -0,0 +1,5 @@
+type=flow
+flow.name=innerFlow
+dependencies=pipeline2
+testprops=moo
+output.override=jobb
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/pipelineFlow.job b/azkaban-test/src/test/resources/executions/embedded2/pipelineFlow.job
new file mode 100644
index 0000000..e50329c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/pipelineFlow.job
@@ -0,0 +1,2 @@
+type=test
+dependencies=pipeline4
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/test1.properties b/azkaban-test/src/test/resources/executions/embedded2/test1.properties
new file mode 100644
index 0000000..120fc25
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/test1.properties
@@ -0,0 +1,4 @@
+param1=test1.1
+param2=test1.2
+param3=test1.3
+param4=test1.4
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded2/test2.properties b/azkaban-test/src/test/resources/executions/embedded2/test2.properties
new file mode 100644
index 0000000..7df7744
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded2/test2.properties
@@ -0,0 +1,4 @@
+param5=test2.5
+param6=test2.6
+param7=test2.7
+param8=test2.8
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded3/innerFlow.job b/azkaban-test/src/test/resources/executions/embedded3/innerFlow.job
new file mode 100644
index 0000000..da71d64
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/innerFlow.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobB,innerJobC
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded3/innerFlow2.job b/azkaban-test/src/test/resources/executions/embedded3/innerFlow2.job
new file mode 100644
index 0000000..e8430ee
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/innerFlow2.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embedded3/innerJobA.job b/azkaban-test/src/test/resources/executions/embedded3/innerJobA.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/innerJobA.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/embedded3/innerJobB.job b/azkaban-test/src/test/resources/executions/embedded3/innerJobB.job
new file mode 100644
index 0000000..24a2e04
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/innerJobB.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow2
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embedded3/innerJobC.job b/azkaban-test/src/test/resources/executions/embedded3/innerJobC.job
new file mode 100644
index 0000000..178bbef
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/innerJobC.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embedded3/joba.job b/azkaban-test/src/test/resources/executions/embedded3/joba.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/joba.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/embedded3/jobb.job b/azkaban-test/src/test/resources/executions/embedded3/jobb.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/jobb.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded3/jobc.job b/azkaban-test/src/test/resources/executions/embedded3/jobc.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/jobc.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded3/jobd.job b/azkaban-test/src/test/resources/executions/embedded3/jobd.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/jobd.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embedded3/jobe.job b/azkaban-test/src/test/resources/executions/embedded3/jobe.job
new file mode 100644
index 0000000..fe986d5
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embedded3/jobe.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=jobb,jobc,jobd
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/innerFlow.job b/azkaban-test/src/test/resources/executions/embeddedBad/innerFlow.job
new file mode 100644
index 0000000..da71d64
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/innerFlow.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobB,innerJobC
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/innerJobA.job b/azkaban-test/src/test/resources/executions/embeddedBad/innerJobA.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/innerJobA.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/innerJobB.job b/azkaban-test/src/test/resources/executions/embeddedBad/innerJobB.job
new file mode 100644
index 0000000..dc29b4a
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/innerJobB.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=jobe
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/innerJobC.job b/azkaban-test/src/test/resources/executions/embeddedBad/innerJobC.job
new file mode 100644
index 0000000..178bbef
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/innerJobC.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=innerJobA
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/joba.job b/azkaban-test/src/test/resources/executions/embeddedBad/joba.job
new file mode 100644
index 0000000..665b38d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/joba.job
@@ -0,0 +1,4 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/jobb.job b/azkaban-test/src/test/resources/executions/embeddedBad/jobb.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/jobb.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/jobc.job b/azkaban-test/src/test/resources/executions/embeddedBad/jobc.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/jobc.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/jobd.job b/azkaban-test/src/test/resources/executions/embeddedBad/jobd.job
new file mode 100644
index 0000000..8844d8c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/jobd.job
@@ -0,0 +1,3 @@
+type=flow
+flow.name=innerFlow
+dependencies=joba
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/jobe.job b/azkaban-test/src/test/resources/executions/embeddedBad/jobe.job
new file mode 100644
index 0000000..fe986d5
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/jobe.job
@@ -0,0 +1,5 @@
+type=javaprocess
+java.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
+dependencies=jobb,jobc,jobd
diff --git a/azkaban-test/src/test/resources/executions/embeddedBad/selfreference.job b/azkaban-test/src/test/resources/executions/embeddedBad/selfreference.job
new file mode 100644
index 0000000..708f351
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/embeddedBad/selfreference.job
@@ -0,0 +1,2 @@
+type=flow
+flow.name=selfreference
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/innerflow.job b/azkaban-test/src/test/resources/executions/execpropstest/innerflow.job
new file mode 100644
index 0000000..18def67
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/innerflow.job
@@ -0,0 +1,6 @@
+type=flow
+flow.name=job4
+dependencies=job2
+props5=innerflow5
+props6=innerflow6
+props8=innerflow8
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/job1.job b/azkaban-test/src/test/resources/executions/execpropstest/job1.job
new file mode 100644
index 0000000..d910f1a
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/job1.job
@@ -0,0 +1,4 @@
+type=test
+props1=job1
+props2=job2
+props8=job8
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/job3.job b/azkaban-test/src/test/resources/executions/execpropstest/job3.job
new file mode 100644
index 0000000..d9be481
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/job3.job
@@ -0,0 +1,3 @@
+type=test
+dependencies=innerflow
+props3=job3
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/moo.properties b/azkaban-test/src/test/resources/executions/execpropstest/moo.properties
new file mode 100644
index 0000000..7e4f399
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/moo.properties
@@ -0,0 +1,3 @@
+props3=moo3
+props4=moo4
+props5=moo5
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/shared.properties b/azkaban-test/src/test/resources/executions/execpropstest/shared.properties
new file mode 100644
index 0000000..29b2b7b
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/shared.properties
@@ -0,0 +1,3 @@
+props1=shared1
+props2=shared2
+props6=shared6
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/subdir/job2.job b/azkaban-test/src/test/resources/executions/execpropstest/subdir/job2.job
new file mode 100644
index 0000000..bfd48e3
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/subdir/job2.job
@@ -0,0 +1,3 @@
+type=test
+props2=job2
+props7=job7
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/subdir/job4.job b/azkaban-test/src/test/resources/executions/execpropstest/subdir/job4.job
new file mode 100644
index 0000000..e26aaa6
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/subdir/job4.job
@@ -0,0 +1,4 @@
+type=test
+dependencies=job1
+props8=job8
+props9=job9
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/execpropstest/subdir/shared.properties b/azkaban-test/src/test/resources/executions/execpropstest/subdir/shared.properties
new file mode 100644
index 0000000..2ba1880
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/execpropstest/subdir/shared.properties
@@ -0,0 +1,2 @@
+props4=shared4
+props8=shared8
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/exec1.flow b/azkaban-test/src/test/resources/executions/exectest1/exec1.flow
new file mode 100644
index 0000000..5ca051c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/exec1.flow
@@ -0,0 +1,139 @@
+{
+ "project.id":1,
+ "version":2,
+ "id" : "derived-member-data",
+ "success.email" : [],
+ "edges" : [ {
+ "source" : "job1",
+ "target" : "job2"
+ }, {
+ "source" : "job2",
+ "target" : "job3"
+ },{
+ "source" : "job2",
+ "target" : "job4"
+ }, {
+ "source" : "job3",
+ "target" : "job5"
+ },{
+ "source" : "job4",
+ "target" : "job5"
+ },{
+ "source" : "job5",
+ "target" : "job7"
+ },{
+ "source" : "job1",
+ "target" : "job6"
+ },{
+ "source" : "job6",
+ "target" : "job7"
+ },{
+ "source" : "job7",
+ "target" : "job8"
+ },
+ {
+ "source" : "job8",
+ "target" : "job10"
+ }
+ ],
+ "failure.email" : [],
+ "nodes" : [ {
+ "propSource" : "prop2.properties",
+ "id" : "job1",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job1.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job2",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job2.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job3",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job3.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job4",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job4.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job5",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job5.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job6",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job6.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job7",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job7.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job8",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job8.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job10",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job10.job",
+ "expectedRuntime" : 1
+ }
+ ],
+ "layedout" : false,
+ "type" : "flow",
+ "props" : [ {
+ "inherits" : "prop1.properties",
+ "source" : "prop2.properties"
+ },{
+ "source" : "prop1.properties"
+ }]
+}
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/exec1-mod.flow b/azkaban-test/src/test/resources/executions/exectest1/exec1-mod.flow
new file mode 100644
index 0000000..3612d58
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/exec1-mod.flow
@@ -0,0 +1,156 @@
+{
+ "project.id":1,
+ "version":2,
+ "id" : "derived-member-data",
+ "success.email" : [],
+ "edges" : [ {
+ "source" : "job1",
+ "target" : "job2"
+ }, {
+ "source" : "job2",
+ "target" : "job3"
+ },{
+ "source" : "job2",
+ "target" : "job4"
+ }, {
+ "source" : "job3",
+ "target" : "job5"
+ },{
+ "source" : "job4",
+ "target" : "job5"
+ },{
+ "source" : "job5",
+ "target" : "job7"
+ },{
+ "source" : "job1",
+ "target" : "job6"
+ },{
+ "source" : "job6",
+ "target" : "job7"
+ },{
+ "source" : "job7",
+ "target" : "job8"
+ },{
+ "source" : "job7",
+ "target" : "job9"
+ },
+ {
+ "source" : "job8",
+ "target" : "job10"
+ },
+ {
+ "source" : "job9",
+ "target" : "job10"
+ }
+ ],
+ "failure.email" : [],
+ "nodes" : [ {
+ "propSource" : "prop2.properties",
+ "id" : "job1",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job1.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job2",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job2.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job3",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job3.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job4",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job4.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job5",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job5.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job6",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job6.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job7",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job7.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job8",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job8.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job9",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job9.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job10",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job10.job",
+ "expectedRuntime" : 1
+ }
+ ],
+ "layedout" : false,
+ "type" : "flow",
+ "props" : [ {
+ "inherits" : "prop1.properties",
+ "source" : "prop2.properties"
+ },{
+ "source" : "prop1.properties"
+ }]
+}
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/exec2.flow b/azkaban-test/src/test/resources/executions/exectest1/exec2.flow
new file mode 100644
index 0000000..7197af9
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/exec2.flow
@@ -0,0 +1,156 @@
+{
+ "project.id":1,
+ "version":2,
+ "id" : "derived-member-data",
+ "success.email" : [],
+ "edges" : [ {
+ "source" : "job1",
+ "target" : "job2d"
+ }, {
+ "source" : "job2d",
+ "target" : "job3"
+ },{
+ "source" : "job2d",
+ "target" : "job4"
+ }, {
+ "source" : "job3",
+ "target" : "job5"
+ },{
+ "source" : "job4",
+ "target" : "job5"
+ },{
+ "source" : "job5",
+ "target" : "job7"
+ },{
+ "source" : "job1",
+ "target" : "job6"
+ },{
+ "source" : "job6",
+ "target" : "job7"
+ },{
+ "source" : "job7",
+ "target" : "job8"
+ },{
+ "source" : "job7",
+ "target" : "job9"
+ },
+ {
+ "source" : "job8",
+ "target" : "job10"
+ },
+ {
+ "source" : "job9",
+ "target" : "job10"
+ }
+ ],
+ "failure.email" : [],
+ "nodes" : [ {
+ "propSource" : "prop2.properties",
+ "id" : "job1",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job1.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job2d",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job2d.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job3",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job3.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job4",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job4.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job5",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job5.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job6",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job6.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job7",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job7.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job8",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job8.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job9",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job9.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job10",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job10.job",
+ "expectedRuntime" : 1
+ }
+ ],
+ "layedout" : false,
+ "type" : "flow",
+ "props" : [ {
+ "inherits" : "prop1.properties",
+ "source" : "prop2.properties"
+ },{
+ "source" : "prop1.properties"
+ }]
+}
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/exec3.flow b/azkaban-test/src/test/resources/executions/exectest1/exec3.flow
new file mode 100644
index 0000000..d1ea3ac
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/exec3.flow
@@ -0,0 +1,156 @@
+{
+ "project.id":1,
+ "version":2,
+ "id" : "derived-member-data",
+ "success.email" : [],
+ "edges" : [ {
+ "source" : "job1",
+ "target" : "job2d"
+ }, {
+ "source" : "job1",
+ "target" : "job3"
+ },{
+ "source" : "job2d",
+ "target" : "job4"
+ }, {
+ "source" : "job2d",
+ "target" : "job5"
+ },{
+ "source" : "job4",
+ "target" : "job6"
+ },{
+ "source" : "job5",
+ "target" : "job6"
+ },{
+ "source" : "job6",
+ "target" : "job10"
+ },{
+ "source" : "job3",
+ "target" : "job7"
+ },{
+ "source" : "job3",
+ "target" : "job8"
+ },{
+ "source" : "job7",
+ "target" : "job9"
+ },
+ {
+ "source" : "job8",
+ "target" : "job9"
+ },
+ {
+ "source" : "job9",
+ "target" : "job10"
+ }
+ ],
+ "failure.email" : [],
+ "nodes" : [ {
+ "propSource" : "prop2.properties",
+ "id" : "job1",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job1.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job2d",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job2d.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job3",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job3.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job4",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job4.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job5",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job5.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job6",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job6.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job7",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job7.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job8",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job8.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job9",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job9.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job10",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job10.job",
+ "expectedRuntime" : 1
+ }
+ ],
+ "layedout" : false,
+ "type" : "flow",
+ "props" : [ {
+ "inherits" : "prop1.properties",
+ "source" : "prop2.properties"
+ },{
+ "source" : "prop1.properties"
+ }]
+}
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/exec4-retry.flow b/azkaban-test/src/test/resources/executions/exectest1/exec4-retry.flow
new file mode 100644
index 0000000..f18a53c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/exec4-retry.flow
@@ -0,0 +1,54 @@
+{
+ "project.id":1,
+ "version":2,
+ "id" : "derived-member-data",
+ "success.email" : [],
+ "edges" : [ {
+ "source" : "job-retry",
+ "target" : "job-pass"
+ },{
+ "source" : "job-pass",
+ "target" : "job-retry-fail"
+ }
+ ],
+ "failure.email" : [],
+ "nodes" : [ {
+ "propSource" : "prop2.properties",
+ "id" : "job-retry",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job-retry.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job-pass",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job-pass.job",
+ "expectedRuntime" : 1
+ },
+ {
+ "propSource" : "prop2.properties",
+ "id" : "job-retry-fail",
+ "jobType" : "java",
+ "layout" : {
+ "level" : 0
+ },
+ "jobSource" : "job-retry-fail.job",
+ "expectedRuntime" : 1
+ }
+ ],
+ "layedout" : false,
+ "type" : "flow",
+ "props" : [ {
+ "inherits" : "prop1.properties",
+ "source" : "prop2.properties"
+ },{
+ "source" : "prop1.properties"
+ }]
+}
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job1.job b/azkaban-test/src/test/resources/executions/exectest1/job1.job
new file mode 100644
index 0000000..0a60dc4
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job1.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job10.job b/azkaban-test/src/test/resources/executions/exectest1/job10.job
new file mode 100644
index 0000000..218f774
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job10.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job8,job9
+seconds=5
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job2.job b/azkaban-test/src/test/resources/executions/exectest1/job2.job
new file mode 100644
index 0000000..3c918c8
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job2.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job1
+seconds=2
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job2d.job b/azkaban-test/src/test/resources/executions/exectest1/job2d.job
new file mode 100644
index 0000000..b4216ba
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job2d.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job1
+seconds=1
+fail=true
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job3.job b/azkaban-test/src/test/resources/executions/exectest1/job3.job
new file mode 100644
index 0000000..b26a76c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job3.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job2
+seconds=3
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job4.job b/azkaban-test/src/test/resources/executions/exectest1/job4.job
new file mode 100644
index 0000000..1eccb73
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job4.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job2
+seconds=8
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job5.job b/azkaban-test/src/test/resources/executions/exectest1/job5.job
new file mode 100644
index 0000000..8dd934d
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job5.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job3,job4
+seconds=5
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job6.job b/azkaban-test/src/test/resources/executions/exectest1/job6.job
new file mode 100644
index 0000000..b5df29c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job6.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job1
+seconds=4
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job7.job b/azkaban-test/src/test/resources/executions/exectest1/job7.job
new file mode 100644
index 0000000..d01cf79
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job7.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job5,job6
+seconds=2
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job8.job b/azkaban-test/src/test/resources/executions/exectest1/job8.job
new file mode 100644
index 0000000..643598c
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job8.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job7
+seconds=3
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job9.job b/azkaban-test/src/test/resources/executions/exectest1/job9.job
new file mode 100644
index 0000000..5d6dda9
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job9.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+dependencies=job7
+seconds=4
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job-pass.job b/azkaban-test/src/test/resources/executions/exectest1/job-pass.job
new file mode 100644
index 0000000..0a60dc4
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job-pass.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job-retry.job b/azkaban-test/src/test/resources/executions/exectest1/job-retry.job
new file mode 100644
index 0000000..94cd0fa
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job-retry.job
@@ -0,0 +1,8 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=true
+passRetry=2
+retries=3
+retry.backoff=1000
+
diff --git a/azkaban-test/src/test/resources/executions/exectest1/job-retry-fail.job b/azkaban-test/src/test/resources/executions/exectest1/job-retry-fail.job
new file mode 100644
index 0000000..bd51b47
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/job-retry-fail.job
@@ -0,0 +1,8 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=1
+fail=true
+passRetry=3
+retries=2
+retry.backoff=2000
+
diff --git a/azkaban-test/src/test/resources/executions/exectest1/prop1.properties b/azkaban-test/src/test/resources/executions/exectest1/prop1.properties
new file mode 100644
index 0000000..fb37c8b
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/prop1.properties
@@ -0,0 +1,2 @@
+a=0
+c=0
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest1/prop2.properties b/azkaban-test/src/test/resources/executions/exectest1/prop2.properties
new file mode 100644
index 0000000..86fbde0
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest1/prop2.properties
@@ -0,0 +1,2 @@
+a=1
+b=2
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest2/failflow.job b/azkaban-test/src/test/resources/executions/exectest2/failflow.job
new file mode 100644
index 0000000..bfd7ce6
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/failflow.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=5
+fail=false
+dependencies=myjob3,myjob5
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob1.job b/azkaban-test/src/test/resources/executions/exectest2/myjob1.job
new file mode 100644
index 0000000..917929e
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob1.job
@@ -0,0 +1,4 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=5
+fail=false
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob2-fail20.job b/azkaban-test/src/test/resources/executions/exectest2/myjob2-fail20.job
new file mode 100644
index 0000000..4e02239
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob2-fail20.job
@@ -0,0 +1,6 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=20
+fail=true
+passRetry=2
+dependencies=myjob1
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob2-fail30.job b/azkaban-test/src/test/resources/executions/exectest2/myjob2-fail30.job
new file mode 100644
index 0000000..908bfed
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob2-fail30.job
@@ -0,0 +1,6 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=30
+fail=true
+passRetry=2
+dependencies=myjob1
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob2-pass50.job b/azkaban-test/src/test/resources/executions/exectest2/myjob2-pass50.job
new file mode 100644
index 0000000..d9553ca
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob2-pass50.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=50
+fail=false
+dependencies=myjob1
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob3.job b/azkaban-test/src/test/resources/executions/exectest2/myjob3.job
new file mode 100644
index 0000000..fe614fa
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob3.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=240
+fail=false
+dependencies=myjob2-pass50
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob4.job b/azkaban-test/src/test/resources/executions/exectest2/myjob4.job
new file mode 100644
index 0000000..d81f9f2
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob4.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=20
+fail=false
+dependencies=myjob2-fail20
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/exectest2/myjob5.job b/azkaban-test/src/test/resources/executions/exectest2/myjob5.job
new file mode 100644
index 0000000..9b17129
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/exectest2/myjob5.job
@@ -0,0 +1,5 @@
+type=java
+job.class=azkaban.test.executor.SleepJavaJob
+seconds=20
+fail=false
+dependencies=myjob4,myjob2-fail30
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/logtest/largeLog1.log.short b/azkaban-test/src/test/resources/executions/logtest/largeLog1.log.short
new file mode 100644
index 0000000..83d535f
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/logtest/largeLog1.log.short
@@ -0,0 +1,68 @@
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an
+...
+Log file is too big. Skipping 101991 in the middle.
+...
+ore.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
+Second verse, same as the first!
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/logtest/largeLog2.log b/azkaban-test/src/test/resources/executions/logtest/largeLog2.log
new file mode 100644
index 0000000..8a0ce3f
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/logtest/largeLog2.log
@@ -0,0 +1,8 @@
+I'm Henery the Eighth, I am,
+Henery the Eighth I am, I am!
+I got married to the widow next door,
+She's been married seven times before.
+And every one was an Henery
+It wouldn't be a Willie or a Sam
+I'm her eighth old man I'm Henery
+Henery the Eighth, I am!
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/logtest/log1.log b/azkaban-test/src/test/resources/executions/logtest/log1.log
new file mode 100644
index 0000000..f721b7e
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/logtest/log1.log
@@ -0,0 +1 @@
+My name is R
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/logtest/log2.log b/azkaban-test/src/test/resources/executions/logtest/log2.log
new file mode 100644
index 0000000..2621270
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/logtest/log2.log
@@ -0,0 +1 @@
+ichard Park. This is a multiple l
\ No newline at end of file
diff --git a/azkaban-test/src/test/resources/executions/logtest/log3.log b/azkaban-test/src/test/resources/executions/logtest/log3.log
new file mode 100644
index 0000000..baa8422
--- /dev/null
+++ b/azkaban-test/src/test/resources/executions/logtest/log3.log
@@ -0,0 +1 @@
+og test.
\ No newline at end of file
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/SchedulerStatistics.java b/azkaban-webserver/src/main/java/azkaban/webapp/SchedulerStatistics.java
new file mode 100644
index 0000000..ea4dabd
--- /dev/null
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/SchedulerStatistics.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+
+import azkaban.executor.ExecutableFlow;
+import azkaban.executor.ExecutorManagerAdapter;
+import azkaban.executor.ExecutorManagerException;
+import azkaban.executor.Status;
+import azkaban.scheduler.Schedule;
+import azkaban.scheduler.ScheduleManager;
+import azkaban.scheduler.ScheduleStatisticManager;
+import azkaban.scheduler.ScheduleManagerException;
+
+public class SchedulerStatistics {
+ public static Map<String, Object> getStatistics(
+ int scheduleId, AzkabanWebServer server)
+ throws ScheduleManagerException {
+ if (ScheduleStatisticManager.getCacheDirectory() == null) {
+ ScheduleStatisticManager.setCacheFolder(
+ new File(server.getServerProps().getString("cache.directory", "cache")));
+ }
+ Map<String, Object> data = ScheduleStatisticManager.loadCache(scheduleId);
+ if (data != null) {
+ return data;
+ }
+
+ // Calculate data and cache it
+ data = calculateStats(scheduleId, server);
+ ScheduleStatisticManager.saveCache(scheduleId, data);
+ return data;
+ }
+
+ private static Map<String, Object> calculateStats(
+ int scheduleId, AzkabanWebServer server)
+ throws ScheduleManagerException {
+ Map<String, Object> data = new HashMap<String, Object>();
+ ExecutorManagerAdapter executorManager = server.getExecutorManager();
+ ScheduleManager scheduleManager = server.getScheduleManager();
+ Schedule schedule = scheduleManager.getSchedule(scheduleId);
+
+ try {
+ List<ExecutableFlow> executables = executorManager.getExecutableFlows(
+ schedule.getProjectId(), schedule.getFlowName(), 0,
+ ScheduleStatisticManager.STAT_NUMBERS, Status.SUCCEEDED);
+
+ long average = 0;
+ long min = Integer.MAX_VALUE;
+ long max = 0;
+ if (executables.isEmpty()) {
+ average = 0;
+ min = 0;
+ max = 0;
+ } else {
+ for (ExecutableFlow flow : executables) {
+ long time = flow.getEndTime() - flow.getStartTime();
+ average += time;
+ if (time < min) {
+ min = time;
+ }
+ if (time > max) {
+ max = time;
+ }
+ }
+ average /= executables.size();
+ }
+
+ data.put("average", average);
+ data.put("min", min);
+ data.put("max", max);
+ } catch (ExecutorManagerException e) {
+ e.printStackTrace();
+ }
+
+ return data;
+ }
+}
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectlogpage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectlogpage.vm
new file mode 100644
index 0000000..5e6a62b
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectlogpage.vm
@@ -0,0 +1,95 @@
+#*
+ * 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.
+*#
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+#parse ("azkaban/webapp/servlet/velocity/style.vm")
+#parse ("azkaban/webapp/servlet/velocity/javascript.vm")
+
+ <script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/project-logs.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/project-modals.js"></script>
+ <script type="text/javascript">
+ var contextURL = "${context}";
+ var currentTime = ${currentTime};
+ var timezone = "${timezone}";
+ var errorMessage = null;
+ var successMessage = null;
+
+ var projectId = ${project.id};
+ var projectName = "$project.name";
+ </script>
+ </head>
+ <body>
+
+#set ($current_page="all")
+#parse ("azkaban/webapp/servlet/velocity/nav.vm")
+
+#if ($errorMsg)
+ #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
+#else
+
+ ## Page header.
+
+ #parse ("azkaban/webapp/servlet/velocity/projectpageheader.vm")
+
+ ## Page content.
+
+ <div class="container-full">
+
+ #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
+
+ <div class="row row-offcanvas row-offcanvas-right">
+ <div class="col-xs-12 col-sm-9">
+
+ #set ($project_page = "logs")
+ #parse ("azkaban/webapp/servlet/velocity/projectnav.vm")
+
+ <div class="panel panel-default" id="flow-tabs">
+ <div class="panel-heading">
+ <div class="pull-right" id="project-options">
+ <button type="button" id="updateLogBtn" class="btn btn-xs btn-info">Refresh</button>
+ </div>
+ Audit Logs
+ </div>
+ <table class="table table-striped" id="logTable">
+ <thead>
+ <tr>
+ <th>Time</th>
+ <th>User</th>
+ <th>Type</th>
+ <th>Message</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="col-xs-6 col-sm-3 sidebar-offcanvas">
+ #parse ("azkaban/webapp/servlet/velocity/projectsidebar.vm")
+ </div>
+ </div>
+
+ #parse ("azkaban/webapp/servlet/velocity/projectmodals.vm")
+
+ </div><!-- /container-full -->
+#end
+ </body>
+</html>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectmodals.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectmodals.vm
new file mode 100644
index 0000000..ccbbbdd
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectmodals.vm
@@ -0,0 +1,71 @@
+#*
+ * 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.
+*#
+
+ ## Upload project modal
+
+ <div class="modal" id="upload-project-modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <form id="upload-project-form" enctype="multipart/form-data" method="post" action="$!context/manager">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">Upload Project Files</h4>
+ </div>
+ <div class="modal-body">
+ <div class="alert alert-danger" id="upload-project-modal-error-msg">$error_msg</div>
+ <fieldset class="form-horizontal">
+ <div class="form-group">
+ <label for="file" class="col-sm-3 control-label">Job Archive</label>
+ <div class="col-sm-9">
+ <input type="file" class="form-control" id="file" name="file">
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ <div class="modal-footer">
+ <input type="hidden" name="project" value="$project.name">
+ <input type="hidden" name="action" value="upload">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-primary" id="upload-project-btn">Upload</button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+
+ ## Delete project modal.
+
+ <div class="modal" id="delete-project-modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">Delete Project</h4>
+ </div>
+ <div class="modal-body">
+ <p><strong>Warning:</strong> This project will be deleted and may not be recoverable.</p>
+ </div>
+ <div class="modal-footer">
+ <form id="delete-form">
+ <input type="hidden" name="project" value="$project.name">
+ <input type="hidden" name="delete" value="true">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger" id="delete-btn">Delete Project</button>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectpage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectpage.vm
new file mode 100644
index 0000000..360c9db
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/projectpage.vm
@@ -0,0 +1,110 @@
+#*
+ * 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.
+*#
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+#parse ("azkaban/webapp/servlet/velocity/style.vm")
+#parse ("azkaban/webapp/servlet/velocity/javascript.vm")
+#parse ("azkaban/webapp/servlet/velocity/svgflowincludes.vm")
+ <script type="text/javascript" src="${context}/js/moment.min.js"></script>
+ <script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/flow-execute-dialog.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/project.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/project-modals.js"></script>
+ <script type="text/javascript">
+ var contextURL = "${context}";
+ var currentTime = ${currentTime};
+ var timezone = "${timezone}";
+ var errorMessage = null;
+ var successMessage = null;
+
+ var projectId = ${project.id};
+ var execAccess = ${exec};
+ var projectName = "$project.name";
+ </script>
+ <link rel="stylesheet" type="text/css" href="${context}/css/bootstrap-datetimepicker.css" />
+ </head>
+ <body>
+
+#set ($current_page="all")
+#parse ("azkaban/webapp/servlet/velocity/nav.vm")
+
+#if ($errorMsg)
+ #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
+#else
+
+ ## Page header.
+
+ #parse ("azkaban/webapp/servlet/velocity/projectpageheader.vm")
+
+ <div class="container-full">
+
+ #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
+
+ ## Page content.
+
+ <div class="row row-offcanvas row-offcanvas-right">
+ <div class="col-xs-12 col-sm-9" id="flow-tabs">
+
+ #set ($project_page = "flows")
+ #parse ("azkaban/webapp/servlet/velocity/projectnav.vm")
+
+ <div id="flow-tabs">
+ #if ($flows)
+ #foreach ($flow in $flows)
+ <div class="panel panel-default" flow="${flow.id}" project="${project.name}">
+ <div class="panel-heading flow-expander" id="${flow.id}">
+ #if (${exec})
+ <div class="pull-right">
+ <button type="button" class="btn btn-xs btn-success execute-flow" flowId="${flow.id}">Execute Flow</button>
+ <a href="${context}/manager?project=${project.name}&flow=${flow.id}#executions" class="btn btn-info btn-xs">Executions</a>
+ <a href="${context}/manager?project=${project.name}&flow=${flow.id}#summary" class="btn btn-info btn-xs">Summary</a>
+ </div>
+ #end
+ <span class="glyphicon glyphicon-chevron-down flow-expander-icon"></span>
+ <a href="${context}/manager?project=${project.name}&flow=${flow.id}">${flow.id}</a>
+ </div>
+ <div id="${flow.id}-child" class="panel-collapse panel-list collapse">
+ <ul class="list-group list-group-collapse expanded-flow-job-list" id="${flow.id}-tbody"></ul>
+ </div>
+ </div>
+ #end
+ #else
+ <div class="callout callout-default">
+ <h4>No Flows</h4>
+ <p>No flows have been uploaded to this project yet.</p>
+ </div>
+ #end
+ </div><!-- /#flow-tabs -->
+ </div><!-- /col-xs-8 -->
+
+ <div class="col-xs-6 col-sm-3 sidebar-offcanvas">
+ #parse ("azkaban/webapp/servlet/velocity/projectsidebar.vm")
+ </div><!-- /col-xs-4 -->
+ </div><!-- /row -->
+
+ #parse ("azkaban/webapp/servlet/velocity/projectmodals.vm")
+ #parse ("azkaban/webapp/servlet/velocity/invalidsessionmodal.vm")
+ #parse ("azkaban/webapp/servlet/velocity/flowexecutionpanel.vm")
+ #parse ("azkaban/webapp/servlet/velocity/messagedialog.vm")
+
+ </div><!-- /container -->
+#end
+ </body>
+</html>
+
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/propertypage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/propertypage.vm
new file mode 100644
index 0000000..3c7e0ce
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/propertypage.vm
@@ -0,0 +1,128 @@
+#*
+ * 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.
+*#
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+
+#parse("azkaban/webapp/servlet/velocity/style.vm")
+#parse ("azkaban/webapp/servlet/velocity/javascript.vm")
+
+ <script type="text/javascript">
+ var contextURL = "${context}";
+ var currentTime = ${currentTime};
+ var timezone = "${timezone}";
+ var errorMessage = null;
+ var successMessage = null;
+
+ var projectId = "$project.name";
+ </script>
+ </head>
+ <body>
+
+#set($current_page="all")
+#parse("azkaban/webapp/servlet/velocity/nav.vm")
+
+#if ($errorMsg)
+ #parse("azkaban/webapp/servlet/velocity/errormsg.vm")
+#else
+
+ ## Page header
+
+ <div class="az-page-header page-header-bare">
+ <div class="container-full">
+ <h1><a href="${context}/manager?project=${project.name}&flow=${flowid}&job=${jobid}&prop=${property}">Properties <small>$property</small></a></h1>
+ </div>
+ </div>
+ <div class="page-breadcrumb">
+ <div class="container-full">
+ <ol class="breadcrumb">
+ <li><a href="${context}/manager?project=${project.name}"><strong>Project</strong> $project.name</a></li>
+ <li><a href="${context}/manager?project=${project.name}&flow=${flowid}"><strong>Flow</strong> $flowid</a></li>
+ <li><a href="${context}/manager?project=${project.name}&flow=${flowid}&job=${jobid}"><strong>Job</strong> $jobid</a></li>
+ <li class="active"><strong>Properties</strong> $property</li>
+ </ol>
+ </div>
+ </div>
+
+ <div class="container-full">
+
+ #parse("azkaban/webapp/servlet/velocity/alerts.vm")
+
+ <div class="row row-offcanvas row-offcanvas-right">
+ <div class="col-xs-12 col-sm-9">
+
+ ## Properties
+
+ <div class="panel panel-default">
+ <div class="panel-heading">$property</div>
+
+ <table class="table table-striped table-bordered properties-table">
+ <thead>
+ <tr>
+ <th class="tb-pname">Parameter Name</th>
+ <th class="tb-pvalue">Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ #foreach ($parameter in $parameters)
+ <tr>
+ <td class="property-key">$parameter.first</td>
+ <td>$parameter.second</td>
+ </tr>
+ #end
+ </tbody>
+ </table>
+ </div>
+ </div><!-- /col-xs-8 -->
+ <div class="col-xs-6 col-sm-3 sidebar-offcanvas">
+ <div class="well" id="job-summary">
+ <h4>Properties <small>$property</small></h4>
+ <p><strong>Job</strong> $jobid</p>
+ </div>
+
+ <div class="panel panel-default">
+ <div class="panel-heading">Inherited From</div>
+ <ul class="list-group">
+ #if ($inheritedproperties)
+ #foreach ($inheritedproperty in $inheritedproperties)
+ <li class="list-group-item"><a href="${context}/manager?project=${project.name}&flow=${flowid}&job=${jobid}&prop=$inheritedproperty">$inheritedproperty</a></li>
+ #end
+ #else
+ <li class="list-group-item">No inherited properties.</li>
+ #end
+ </ul>
+ </div>
+
+ <div class="panel panel-default">
+ <div class="panel-heading">Source of</div>
+ <ul class="list-group">
+ #if ($dependingproperties)
+ #foreach ($dependingproperty in $dependingproperties)
+ <li class="list-group-item"><a href="${context}/manager?project=${project.name}&flow=${flowid}&job=${jobid}&prop=$dependingproperty">$dependingproperty</a></li>
+ #end
+ #else
+ <li class="list-group-item">No dependents.</li>
+ #end
+ </ul>
+ </div>
+ </div>
+ </div><!-- /row -->
+
+ </div><!-- /container-full -->
+#end
+ </body>
+</html>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
new file mode 100644
index 0000000..bcb4221
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowcalendarpage.vm
@@ -0,0 +1,78 @@
+#*
+ * 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.
+*#
+
+<!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/jquery/jquery.svg.min.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/schedule-svg.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/context-menu.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/svg-navigate.js"></script>
+ <script type="text/javascript">
+ var contextURL = "${context}";
+ var currentTime = ${currentTime};
+ var timezone = "${timezone}";
+ var errorMessage = null;
+ var successMessage = null;
+ </script>
+ <link rel="stylesheet" type="text/css" href="${context}/css/jquery.svg.css" />
+ </head>
+ <body>
+
+#set ($current_page="schedule")
+#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>Scheduled Flows</h1>
+ </div>
+ </div>
+
+ #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
+
+ <div class="container-full">
+ <div class="row">
+ <div class="col-xs-12">
+ <div class="pull-right">
+ <button type="button" class="nav-prev-week btn btn-default">Previous Week</button>
+ <button type="button" class="nav-this-week btn btn-default">Today</button>
+ <button type="button" class="nav-next-week btn btn-default">Next Week</button>
+ </div>
+ <div id="svgDivCustom"></div>
+ </div>
+ </div>
+
+ <div id="contextMenu"></div>
+ </div>
+#end
+ </body>
+</html>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
new file mode 100644
index 0000000..6e7c61b
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
@@ -0,0 +1,117 @@
+#*
+ * 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.
+*#
+
+<!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/bootstrap-datetimepicker.css" />
+
+ <script type="text/javascript" src="${context}/js/moment.min.js"></script>
+ <script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/table-sort.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/schedule-sla.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/scheduled.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/schedule.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="schedule")
+#parse ("azkaban/webapp/servlet/velocity/nav.vm")
+
+#if ($errorMsg)
+ #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
+#else
+
+ ## Page header.
+
+ <div class="az-page-header">
+ <div class="container-full">
+ <h1><a href="${context}/schedule">Scheduled Flows</a></h1>
+ </div>
+ </div>
+
+ ## Page content.
+
+ <div class="container-full">
+
+ #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
+
+ <div class="row">
+ <div class="col-xs-12">
+ <table id="scheduledFlowsTbl" class="table table-striped table-condensed table-bordered table-hover">
+ <thead>
+ <tr>
+ <!--th class="execid">Execution Id</th-->
+ <th>ID</th>
+ <th>Flow</th>
+ <th>Project</th>
+ <th>Submitted By</th>
+ <th class="date">First Scheduled to Run</th>
+ <th class="date">Next Execution Time</th>
+ <th class="date">Repeats Every</th>
+ <th>Has SLA</th>
+ <th colspan="2" class="action ignoresort">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ #if(!$schedules.isEmpty())
+ #foreach($sched in $schedules)
+ <tr>
+ <td>${sched.scheduleId}</td>
+ <td class="tb-name">
+ <a href="${context}/manager?project=${sched.projectName}&flow=${sched.flowName}">${sched.flowName}</a>
+ </td>
+ <td>
+ <a href="${context}/manager?project=${sched.projectName}">${sched.projectName}</a>
+ </td>
+ <td>${sched.submitUser}</td>
+ <td>$utils.formatDateTime(${sched.firstSchedTime})</td>
+ <td>$utils.formatDateTime(${sched.nextExecTime})</td>
+ <td>$utils.formatPeriod(${sched.period})</td>
+ <td>#if(${sched.slaOptions}) true #else false #end</td>
+ <td><button type="button" id="removeSchedBtn" class="btn btn-sm btn-danger" onclick="removeSched(${sched.scheduleId})" >Remove Schedule</button></td>
+ <td><button type="button" id="addSlaBtn" class="btn btn-sm btn-primary" onclick="slaView.initFromSched(${sched.scheduleId}, '${sched.flowName}')" >Set SLA</button></td>
+ </tr>
+ #end
+ #else
+ <tr>
+ <td colspan="10">No scheduled flow found.</td>
+ </tr>
+ #end
+ </tbody>
+ </table>
+ </div><!-- /col-xs-12 -->
+ </div><!-- /row -->
+
+ ## Set SLA modal.
+
+ #parse ("azkaban/webapp/servlet/velocity/slapanel.vm")
+
+ </div>
+#end
+ </body>
+</html>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduleoptionspanel.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduleoptionspanel.vm
new file mode 100644
index 0000000..627e042
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduleoptionspanel.vm
@@ -0,0 +1,175 @@
+#*
+ * 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.
+*#
+
+<div id="scheduleModalBackground" class="modalBackground2">
+ <div id="schedule-options" class="modal modalContainer2">
+ <a href='#' title='Close' class='modal-close'>x</a>
+ <h3>Schedule Flow Options</h3>
+ <div>
+ <ul class="optionsPicker">
+ <li id="scheduleGeneralOptions">General Options</li>
+ <li id="scheduleFlowOptions">Flow Options</li>
+ <!--li id="scheduleSlaOptions">SLA Options</li-->
+ </ul>
+ </div>
+ <div class="optionsPane">
+ <!--div id="scheduleSlaPanel" class="generalPanel panel">
+ <div id="slaActions">
+ <h4>SLA Alert Emails</h4>
+ <dl>
+ <dt >SLA Alert Emails</dt>
+ <dd>
+ <textarea id="slaEmails"></textarea>
+ </dd>
+ </dl>
+ </div>
+ <div id="slaRules">
+ <h4>Flow SLA Rules</h4>
+ <div class="tableDiv">
+ <table id="flowRulesTbl">
+ <thead>
+ <tr>
+ <th>Flow/Job</th>
+ <th>Finish In</th>
+ <th>Email Action</th>
+ <th>Kill Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+ </div>
+ <h4>Job SLA Rules</h4>
+ <div class="tableDiv">
+ <table id="jobRulesTbl">
+ <thead>
+ <tr>
+ <th>Flow/Job</th>
+ <th>Finish In</th>
+ <th>Email Action</th>
+ <th>Kill Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div-->
+ <div id="scheduleGeneralPanel" class="generalPanel panel">
+ <div id="scheduleInfo">
+ <dl>
+ <dt>Schedule Time</dt>
+ <dd>
+ <input id="advhour" type="text" size="2" value="12"/>
+ <input id="advminutes" type="text" size="2" value="00"/>
+ <select id="advam_pm">
+ <option>pm</option>
+ <option>am</option>
+ </select>
+ <select id="advtimezone">
+ <option>PDT</option>
+ <option>UTC</option>
+ </select>
+ </dd>
+ <dt>Schedule Date</dt><dd><input type="text" id="advdatepicker" /></dd>
+ <dt>Recurrence</dt>
+ <dd>
+ <input id="advis_recurring" type="checkbox" checked />
+ <span>repeat every</span>
+ <input id="advperiod" type="text" size="2" value="1"/>
+ <select id="advperiod_units">
+ <option value="d">Days</option>
+ <option value="h">Hours</option>
+ <option value="m">Minutes</option>
+ <option value="M">Months</option>
+ <option value="w">Weeks</option>
+ </select>
+ </dd>
+ </dl>
+ </div>
+ <br></br>
+ <br></br>
+ <div id="scheduleCompleteActions">
+ <h4>Completion Actions</h4>
+ <dl>
+ <dt>Failure Action</dt>
+ <dd>
+ <select id="scheduleFailureAction" name="failureAction">
+ <option value="finishCurrent">Finish Current Running</option>
+ <option value="cancelImmediately">Cancel All</option>
+ <option value="finishPossible">Finish All Possible</option>
+ </select>
+ </dd>
+ <dt>Failure Email</dt>
+ <dd>
+ <textarea id="scheduleFailureEmails"></textarea>
+ </dd>
+ <dt>Notify on Failure</dt>
+ <dd>
+ <input id="scheduleNotifyFailureFirst" class="checkbox" type="checkbox" name="notify" value="first" checked >First Failure</input>
+ <input id="scheduleNotifyFailureLast" class="checkbox" type="checkbox" name="notify" value="last">Flow Stop</input>
+ </dd>
+ <dt>Success Email</dt>
+ <dd>
+ <textarea id="scheduleSuccessEmails"></textarea>
+ </dd>
+ <dt>Concurrent Execution</dt>
+ <dd id="scheduleExecutingJob" class="disabled">
+ <input id="scheduleIgnore" class="radio" type="radio" name="concurrent" value="ignore" checked /><label class="radioLabel" for="ignore">Run Concurrently</label>
+ <input id="schedulePipeline" class="radio" type="radio" name="concurrent" value="pipeline" /><label class="radioLabel" for="pipeline">Pipeline</label>
+ <input id="scheduleQueue" class="radio" type="radio" name="concurrent" value="queue" /><label class="radioLabel" for="queue">Queue Job</label>
+ </dd>
+ </dl>
+ </div>
+ <div id="scheduleFlowPropertyOverride">
+ <h4>Flow Property Override</h4>
+ <div class="tableDiv">
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="scheduleAddRow"><td id="addRow-col" colspan="2"><span class="addIcon"></span><a href="#">Add Row</a></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div id="scheduleGraphPanel" class="graphPanel panel">
+ <div id="scheduleJobListCustom" class="jobList">
+ <div class="filterList">
+ <input class="filter" placeholder=" Job Filter" />
+ </div>
+ <div class="list">
+ </div>
+ <div class="btn5 resetPanZoomBtn" >Reset Pan Zoom</div>
+ </div>
+ <div id="scheduleSvgDivCustom" class="svgDiv" >
+ <svg class="svgGraph" xmlns="http://www.w3.org/2000/svg" version="1.1" shape-rendering="optimize-speed" text-rendering="optimize-speed" >
+ </svg>
+ </div>
+ </div>
+ </div>
+ <div class="actions">
+ <a class="yes btn1" id="adv-schedule-btn" href="#">Schedule</a>
+ <a class="no simplemodal-close btn3" id="schedule-cancel-btn" href="#">Cancel</a>
+ </div>
+ </div>
+</div>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
new file mode 100644
index 0000000..8aff70f
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
@@ -0,0 +1,76 @@
+#*
+ * 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.
+*#
+
+ <script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/schedule-panel.js"></script>
+
+ <div class="modal" id="schedule-modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">Schedule Flow Options</h4>
+ </div><!-- /modal-header -->
+ <div class="modal-body">
+ <fieldset class="form-horizontal">
+ <div class="form-group">
+ <label class="col-sm-2 control-label">Time</label>
+ <div class="col-sm-7">
+ <input type="text" id="timepicker" class="form-control">
+ </div>
+ <div class="col-sm-3">
+ <select id="timezone" class="form-control">
+ <option>${timezone}</option>
+ <option>UTC</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2 control-label">Date</label>
+ <div class="col-sm-10">
+ <input type="text" id="datepicker" class="form-control">
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2">Recurrence</label>
+ <div class="col-sm-3">
+ <div class="checkbox">
+ <input id="is_recurring" type="checkbox" checked="checked">
+ <label>repeat every</label>
+ </div>
+ </div>
+ <div class="col-sm-2">
+ <input id="period" type="text" size="2" value="1" class="form-control">
+ </div>
+ <div class="col-sm-3">
+ <select id="period_units" class="form-control">
+ <option value="d">Days</option>
+ <option value="h">Hours</option>
+ <option value="m">Minutes</option>
+ <option value="M">Months</option>
+ <option value="w">Weeks</option>
+ </select>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-success" id="schedule-button">Schedule</button>
+ </div>
+ </div>
+ </div>
+ </div>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/slapanel.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/slapanel.vm
new file mode 100644
index 0000000..dd25911
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/slapanel.vm
@@ -0,0 +1,60 @@
+#*
+ * 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.
+*#
+
+ <div class="modal modal-wide" id="sla-options">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+ <h4 class="modal-title">SLA Options</h4>
+ </div>
+ <div class="modal-body">
+ <h4>SLA Alert Emails</h4>
+ <fieldset>
+ <div class="form-group">
+ <label>SLA Alert Emails</label>
+ <textarea id="slaEmails" class="form-control"></textarea>
+ </div>
+ </fieldset>
+ <h4>Flow SLA Rules</h4>
+ <table class="table table-striped" id="flowRulesTbl">
+ <thead>
+ <tr>
+ <th>Flow/Job</th>
+ <th>Sla Rule</th>
+ <th>Duration</th>
+ <th>Email Action</th>
+ <th>Kill Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="addRow">
+ <td id="addRow-col" colspan="5">
+ <span class="addIcon"></span>
+ <button type="button" class="btn btn-xs btn-success" id="add-btn">Add Row</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <!--<button type="button" class="btn btn-danger" id="remove-sla-btn">Remove SLA</button>-->
+ <button type="button" class="btn btn-primary" id="set-sla-btn">Set/Change SLA</button>
+ </div>
+ </div>
+ </div>
+ </div>
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/svgflowincludes.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/svgflowincludes.vm
new file mode 100644
index 0000000..c669d26
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/svgflowincludes.vm
@@ -0,0 +1,37 @@
+#*
+ * 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.
+*#
+
+ <script type="text/javascript" src="${context}/js/jquery.svg.min.js"></script>
+ <script type="text/javascript" src="${context}/js/jquery.svganim.min.js"></script>
+ <script type="text/javascript" src="${context}/js/jquery.svgfilter.min.js"></script>
+
+ <script type="text/javascript" src="${context}/js/azkaban/util/common.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/date.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+
+ <script type="text/javascript" src="${context}/js/azkaban/util/svgutils.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/svg-navigate.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/layout.js"></script>
+
+ <script type="text/javascript" src="${context}/js/azkaban/view/context-menu.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/util/job-status.js"></script>
+
+ <script type="text/javascript" src="${context}/js/azkaban/util/flow-loader.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/job-list.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/model/svg-graph.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/svg-graph.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" />
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/triggerspage.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/triggerspage.vm
new file mode 100644
index 0000000..7e5afcf
--- /dev/null
+++ b/azkaban-webserver/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>
diff --git a/azkaban-webserver/src/main/tl/.gitignore b/azkaban-webserver/src/main/tl/.gitignore
new file mode 100644
index 0000000..2416a67
--- /dev/null
+++ b/azkaban-webserver/src/main/tl/.gitignore
@@ -0,0 +1 @@
+obj/
azkaban-webserver/src/main/tl/flowstats.tl 155(+155 -0)
diff --git a/azkaban-webserver/src/main/tl/flowstats.tl b/azkaban-webserver/src/main/tl/flowstats.tl
new file mode 100644
index 0000000..5276ce8
--- /dev/null
+++ b/azkaban-webserver/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>
azkaban-webserver/src/main/tl/flowsummary.tl 69(+69 -0)
diff --git a/azkaban-webserver/src/main/tl/flowsummary.tl b/azkaban-webserver/src/main/tl/flowsummary.tl
new file mode 100644
index 0000000..82627d6
--- /dev/null
+++ b/azkaban-webserver/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/azkaban-webserver/src/package/bin/azkaban-web-start.sh b/azkaban-webserver/src/package/bin/azkaban-web-start.sh
new file mode 100755
index 0000000..744320a
--- /dev/null
+++ b/azkaban-webserver/src/package/bin/azkaban-web-start.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+
+azkaban_dir=$(dirname $0)/..
+
+if [[ -z "$tmpdir" ]]; then
+tmpdir=/tmp
+fi
+
+for file in $azkaban_dir/lib/*.jar;
+do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+for file in $azkaban_dir/extlib/*.jar;
+do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+for file in $azkaban_dir/plugins/*/*.jar;
+do
+ CLASSPATH=$CLASSPATH:$file
+done
+
+if [ "$HADOOP_HOME" != "" ]; then
+ echo "Using Hadoop from $HADOOP_HOME"
+ CLASSPATH=$CLASSPATH:$HADOOP_HOME/conf:$HADOOP_HOME/*
+ JAVA_LIB_PATH="-Djava.library.path=$HADOOP_HOME/lib/native/Linux-amd64-64"
+else
+ echo "Error: HADOOP_HOME is not set. Hadoop job types will not run properly."
+fi
+
+if [ "$HIVE_HOME" != "" ]; then
+ echo "Using Hive from $HIVE_HOME"
+ CLASSPATH=$CLASSPATH:$HIVE_HOME/conf:$HIVE_HOME/lib/*
+fi
+
+echo $azkaban_dir;
+echo $CLASSPATH;
+
+executorport=`cat $azkaban_dir/conf/azkaban.properties | grep executor.port | cut -d = -f 2`
+serverpath=`pwd`
+
+if [ -z $AZKABAN_OPTS ]; then
+ AZKABAN_OPTS="-Xmx4G"
+fi
+AZKABAN_OPTS="$AZKABAN_OPTS -server -Dcom.sun.management.jmxremote -Djava.io.tmpdir=$tmpdir -Dexecutorport=$executorport -Dserverpath=$serverpath"
+
+java $AZKABAN_OPTS $JAVA_LIB_PATH -cp $CLASSPATH azkaban.webapp.AzkabanWebServer -conf $azkaban_dir/conf $@ &
+
+echo $! > $azkaban_dir/currentpid
+
diff --git a/azkaban-webserver/src/package/bin/start-web.sh b/azkaban-webserver/src/package/bin/start-web.sh
new file mode 100755
index 0000000..b063aaa
--- /dev/null
+++ b/azkaban-webserver/src/package/bin/start-web.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+base_dir=$(dirname $0)/..
+
+bin/azkaban-web-start.sh $base_dir 2>&1>logs/webServerLog_`date +%F+%T`.out &
diff --git a/azkaban-webserver/src/package/conf/azkaban.properties b/azkaban-webserver/src/package/conf/azkaban.properties
new file mode 100644
index 0000000..609a917
--- /dev/null
+++ b/azkaban-webserver/src/package/conf/azkaban.properties
@@ -0,0 +1,53 @@
+# Azkaban Personalization Settings
+azkaban.name=Test
+azkaban.label=My Local Azkaban
+azkaban.color=#FF3601
+azkaban.default.servlet.path=/index
+web.resource.dir=web/
+default.timezone.id=America/Los_Angeles
+
+# Azkaban UserManager class
+user.manager.class=azkaban.user.XmlUserManager
+user.manager.xml.file=conf/azkaban-users.xml
+
+# Loader for projects
+executor.global.properties=conf/global.properties
+azkaban.project.dir=projects
+
+database.type=mysql
+mysql.port=3306
+mysql.host=localhost
+mysql.database=azkaban
+mysql.user=azkaban
+mysql.password=azkaban
+mysql.numconnections=100
+
+# Velocity dev mode
+velocity.dev.mode=false
+
+# Azkaban Jetty server properties.
+jetty.maxThreads=25
+jetty.ssl.port=8443
+jetty.port=8081
+jetty.keystore=keystore
+jetty.password=password
+jetty.keypassword=password
+jetty.truststore=keystore
+jetty.trustpassword=password
+
+# Azkaban Executor settings
+executor.port=12321
+
+# mail settings
+mail.sender=
+mail.host=
+job.failure.email=
+job.success.email=
+
+lockdown.create.projects=false
+
+cache.directory=cache
+
+# JMX stats
+jetty.connector.stats=true
+executor.connector.stats=true
diff --git a/azkaban-webserver/src/restli/.gitignore b/azkaban-webserver/src/restli/.gitignore
new file mode 100644
index 0000000..a6ad54c
--- /dev/null
+++ b/azkaban-webserver/src/restli/.gitignore
@@ -0,0 +1,2 @@
+generatedJava
+generatedRestSpec
diff --git a/azkaban-webserver/src/restli/java/azkaban/restli/ProjectManagerResource.java b/azkaban-webserver/src/restli/java/azkaban/restli/ProjectManagerResource.java
new file mode 100644
index 0000000..a3b61b8
--- /dev/null
+++ b/azkaban-webserver/src/restli/java/azkaban/restli/ProjectManagerResource.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2014 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.restli;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import javax.servlet.ServletException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+import azkaban.project.Project;
+import azkaban.project.ProjectManager;
+import azkaban.project.ProjectManagerException;
+import azkaban.user.Permission;
+import azkaban.user.User;
+import azkaban.user.UserManagerException;
+import azkaban.utils.Utils;
+import azkaban.webapp.AzkabanWebServer;
+
+import com.linkedin.restli.server.annotations.Action;
+import com.linkedin.restli.server.annotations.ActionParam;
+import com.linkedin.restli.server.annotations.RestLiActions;
+import com.linkedin.restli.server.resources.ResourceContextHolder;
+
+@RestLiActions(name = "project", namespace = "azkaban.restli")
+public class ProjectManagerResource extends ResourceContextHolder {
+ private static final Logger logger = Logger
+ .getLogger(ProjectManagerResource.class);
+
+ public AzkabanWebServer getAzkaban() {
+ return AzkabanWebServer.getInstance();
+ }
+
+ @Action(name = "deploy")
+ public String deploy(@ActionParam("sessionId") String sessionId,
+ @ActionParam("projectName") String projectName,
+ @ActionParam("packageUrl") String packageUrl)
+ throws ProjectManagerException, UserManagerException, ServletException,
+ IOException {
+ logger.info("Deploy called. {sessionId: " + sessionId + ", projectName: "
+ + projectName + ", packageUrl:" + packageUrl + "}");
+
+ String ip =
+ (String) this.getContext().getRawRequestContext()
+ .getLocalAttr("REMOTE_ADDR");
+ User user = ResourceUtils.getUserFromSessionId(sessionId, ip);
+ ProjectManager projectManager = getAzkaban().getProjectManager();
+ Project project = projectManager.getProject(projectName);
+ if (project == null) {
+ throw new ProjectManagerException("Project '" + projectName
+ + "' not found.");
+ }
+
+ if (!ResourceUtils.hasPermission(project, user, Permission.Type.WRITE)) {
+ String errorMsg =
+ "User " + user.getUserId()
+ + " has no permission to write to project " + project.getName();
+ logger.error(errorMsg);
+ throw new ProjectManagerException(errorMsg);
+ }
+
+ logger.info("Target package URL is " + packageUrl);
+ URL url = null;
+ try {
+ url = new URL(packageUrl);
+ } catch (MalformedURLException e) {
+ String errorMsg = "URL " + packageUrl + " is malformed.";
+ logger.error(errorMsg, e);
+ throw new ProjectManagerException(errorMsg, e);
+ }
+
+ String filename = getFileName(url.getFile());
+ File tempDir = Utils.createTempDir();
+ File archiveFile = new File(tempDir, filename);
+ try {
+ // Since zip files can be large, don't specify an explicit read or
+ // connection
+ // timeout. This will cause the call to block until the download is
+ // complete.
+ logger.info("Downloading package from " + packageUrl);
+ FileUtils.copyURLToFile(url, archiveFile);
+
+ logger.info("Downloaded to " + archiveFile.toString());
+ projectManager.uploadProject(project, archiveFile, "zip", user);
+ } catch (IOException e) {
+ String errorMsg =
+ "Download of URL " + packageUrl + " to " + archiveFile.toString()
+ + " failed";
+ logger.error(errorMsg, e);
+ throw new ProjectManagerException(errorMsg, e);
+ } finally {
+ if (tempDir.exists()) {
+ FileUtils.deleteDirectory(tempDir);
+ }
+ }
+ return Integer.toString(project.getVersion());
+ }
+
+ private String getFileName(String file) {
+ return file.substring(file.lastIndexOf("/") + 1);
+ }
+}
diff --git a/azkaban-webserver/src/restli/java/azkaban/restli/ResourceUtils.java b/azkaban-webserver/src/restli/java/azkaban/restli/ResourceUtils.java
new file mode 100644
index 0000000..36be301
--- /dev/null
+++ b/azkaban-webserver/src/restli/java/azkaban/restli/ResourceUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2014 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.restli;
+
+import azkaban.project.Project;
+import azkaban.user.Permission;
+import azkaban.user.Role;
+import azkaban.user.User;
+import azkaban.user.UserManager;
+import azkaban.user.UserManagerException;
+import azkaban.webapp.AzkabanWebServer;
+import azkaban.server.session.Session;
+
+public class ResourceUtils {
+
+ public static boolean hasPermission(Project project, User user,
+ Permission.Type type) {
+ UserManager userManager = AzkabanWebServer.getInstance().getUserManager();
+ if (project.hasPermission(user, type)) {
+ return true;
+ }
+
+ for (String roleName : user.getRoles()) {
+ Role role = userManager.getRole(roleName);
+ if (role.getPermission().isPermissionSet(type)
+ || role.getPermission().isPermissionSet(Permission.Type.ADMIN)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static User getUserFromSessionId(String sessionId, String ip)
+ throws UserManagerException {
+ Session session =
+ AzkabanWebServer.getInstance().getSessionCache().getSession(sessionId);
+ if (session == null) {
+ throw new UserManagerException("Invalid session. Login required");
+ } else if (!session.getIp().equals(ip)) {
+ throw new UserManagerException("Invalid session. Session expired.");
+ }
+
+ return session.getUser();
+ }
+}
diff --git a/azkaban-webserver/src/restli/java/azkaban/restli/UserManagerResource.java b/azkaban-webserver/src/restli/java/azkaban/restli/UserManagerResource.java
new file mode 100644
index 0000000..d3c0c33
--- /dev/null
+++ b/azkaban-webserver/src/restli/java/azkaban/restli/UserManagerResource.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2014 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.restli;
+
+import java.util.UUID;
+import javax.servlet.ServletException;
+import org.apache.log4j.Logger;
+
+import azkaban.restli.user.User;
+import azkaban.user.UserManager;
+import azkaban.user.UserManagerException;
+import azkaban.webapp.AzkabanWebServer;
+import azkaban.server.session.Session;
+
+import com.linkedin.restli.server.annotations.Action;
+import com.linkedin.restli.server.annotations.ActionParam;
+import com.linkedin.restli.server.annotations.RestLiActions;
+import com.linkedin.restli.server.resources.ResourceContextHolder;
+
+@RestLiActions(name = "user", namespace = "azkaban.restli")
+public class UserManagerResource extends ResourceContextHolder {
+ private static final Logger logger = Logger
+ .getLogger(UserManagerResource.class);
+
+ public AzkabanWebServer getAzkaban() {
+ return AzkabanWebServer.getInstance();
+ }
+
+ @Action(name = "login")
+ public String login(@ActionParam("username") String username,
+ @ActionParam("password") String password) throws UserManagerException,
+ ServletException {
+ String ip =
+ (String) this.getContext().getRawRequestContext()
+ .getLocalAttr("REMOTE_ADDR");
+ logger
+ .info("Attempting to login for " + username + " from ip '" + ip + "'");
+
+ Session session = createSession(username, password, ip);
+
+ logger.info("Session id " + session.getSessionId() + " created for user '"
+ + username + "' and ip " + ip);
+ return session.getSessionId();
+ }
+
+ @Action(name = "getUserFromSessionId")
+ public User getUserFromSessionId(@ActionParam("sessionId") String sessionId) {
+ String ip =
+ (String) this.getContext().getRawRequestContext()
+ .getLocalAttr("REMOTE_ADDR");
+ Session session = getSessionFromSessionId(sessionId, ip);
+ azkaban.user.User azUser = session.getUser();
+
+ // Fill out the restli object with properties from the Azkaban user
+ User user = new User();
+ user.setUserId(azUser.getUserId());
+ user.setEmail(azUser.getEmail());
+ return user;
+ }
+
+ private Session createSession(String username, String password, String ip)
+ throws UserManagerException, ServletException {
+ UserManager manager = getAzkaban().getUserManager();
+ azkaban.user.User user = manager.getUser(username, password);
+
+ String randomUID = UUID.randomUUID().toString();
+ Session session = new Session(randomUID, user, ip);
+ getAzkaban().getSessionCache().addSession(session);
+
+ return session;
+ }
+
+ private Session getSessionFromSessionId(String sessionId, String remoteIp) {
+ if (sessionId == null) {
+ return null;
+ }
+
+ Session session = getAzkaban().getSessionCache().getSession(sessionId);
+ // Check if the IP's are equal. If not, we invalidate the sesson.
+ if (session == null || !remoteIp.equals(session.getIp())) {
+ return null;
+ }
+
+ return session;
+ }
+}
diff --git a/azkaban-webserver/src/web/css/bootstrap-datetimepicker.css b/azkaban-webserver/src/web/css/bootstrap-datetimepicker.css
new file mode 100644
index 0000000..e5eb7a6
--- /dev/null
+++ b/azkaban-webserver/src/web/css/bootstrap-datetimepicker.css
@@ -0,0 +1,174 @@
+/**
+ * Build file for the dist version of datetimepicker.css
+ */
+/*!
+ * Datetimepicker for Bootstrap v3
+ * https://github.com/Eonasdan/bootstrap-datetimepicker/
+ * Copyright 2012 Stefan Petre
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.bootstrap-datetimepicker-widget {
+ top: 0;
+ left: 0;
+ width: 250px;
+ padding: 4px;
+ margin-top: 1px;
+ z-index: 9999;
+ border-radius: 4px;
+ /*.dow {
+ border-top: 1px solid #ddd !important;
+ }*/
+}
+.bootstrap-datetimepicker-widget .btn {
+ padding: 6px;
+}
+.bootstrap-datetimepicker-widget:before {
+ content: '';
+ display: inline-block;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ position: absolute;
+ top: -7px;
+ left: 6px;
+}
+.bootstrap-datetimepicker-widget:after {
+ content: '';
+ display: inline-block;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid white;
+ position: absolute;
+ top: -6px;
+ left: 7px;
+}
+.bootstrap-datetimepicker-widget.pull-right:before {
+ left: auto;
+ right: 6px;
+}
+.bootstrap-datetimepicker-widget.pull-right:after {
+ left: auto;
+ right: 7px;
+}
+.bootstrap-datetimepicker-widget > ul {
+ list-style-type: none;
+ margin: 0;
+}
+.bootstrap-datetimepicker-widget .timepicker-hour,
+.bootstrap-datetimepicker-widget .timepicker-minute,
+.bootstrap-datetimepicker-widget .timepicker-second {
+ width: 100%;
+ font-weight: bold;
+ font-size: 1.2em;
+}
+.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator {
+ width: 4px;
+ padding: 0;
+ margin: 0;
+}
+.bootstrap-datetimepicker-widget .datepicker > div {
+ display: none;
+}
+.bootstrap-datetimepicker-widget .picker-switch {
+ text-align: center;
+}
+.bootstrap-datetimepicker-widget table {
+ width: 100%;
+ margin: 0;
+}
+.bootstrap-datetimepicker-widget td,
+.bootstrap-datetimepicker-widget th {
+ text-align: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 4px;
+}
+.bootstrap-datetimepicker-widget td.day:hover,
+.bootstrap-datetimepicker-widget td.hour:hover,
+.bootstrap-datetimepicker-widget td.minute:hover,
+.bootstrap-datetimepicker-widget td.second:hover {
+ background: #eeeeee;
+ cursor: pointer;
+}
+.bootstrap-datetimepicker-widget td.old,
+.bootstrap-datetimepicker-widget td.new {
+ color: #999999;
+}
+.bootstrap-datetimepicker-widget td.active,
+.bootstrap-datetimepicker-widget td.active:hover {
+ background-color: #428bca;
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.bootstrap-datetimepicker-widget td.disabled,
+.bootstrap-datetimepicker-widget td.disabled:hover {
+ background: none;
+ color: #999999;
+ cursor: not-allowed;
+}
+.bootstrap-datetimepicker-widget td span {
+ display: block;
+ width: 47px;
+ height: 54px;
+ line-height: 54px;
+ float: left;
+ margin: 2px;
+ cursor: pointer;
+ border-radius: 4px;
+}
+.bootstrap-datetimepicker-widget td span:hover {
+ background: #eeeeee;
+}
+.bootstrap-datetimepicker-widget td span.active {
+ background-color: #428bca;
+ color: #fff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.bootstrap-datetimepicker-widget td span.old {
+ color: #999999;
+}
+.bootstrap-datetimepicker-widget td span.disabled,
+.bootstrap-datetimepicker-widget td span.disabled:hover {
+ background: none;
+ color: #999999;
+ cursor: not-allowed;
+}
+.bootstrap-datetimepicker-widget th.switch {
+ width: 145px;
+}
+.bootstrap-datetimepicker-widget th.next,
+.bootstrap-datetimepicker-widget th.prev {
+ font-size: 21px;
+}
+.bootstrap-datetimepicker-widget th.disabled,
+.bootstrap-datetimepicker-widget th.disabled:hover {
+ background: none;
+ color: #999999;
+ cursor: not-allowed;
+}
+.bootstrap-datetimepicker-widget thead tr:first-child th {
+ cursor: pointer;
+}
+.bootstrap-datetimepicker-widget thead tr:first-child th:hover {
+ background: #eeeeee;
+}
+.input-group.date .input-group-addon span {
+ display: block;
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
+.bootstrap-datetimepicker-widget.left-oriented:before {
+ left: auto;
+ right: 6px;
+}
+.bootstrap-datetimepicker-widget.left-oriented:after {
+ left: auto;
+ right: 7px;
+}
+.bootstrap-datetimepicker-widget ul.list-unstyled li.in div.timepicker div.timepicker-picker table.table-condensed tbody > tr > td {
+ padding: 0px !important;
+}
diff --git a/azkaban-webserver/src/web/css/images/addIcon.png b/azkaban-webserver/src/web/css/images/addIcon.png
new file mode 100644
index 0000000..f079305
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/addIcon.png differ
diff --git a/azkaban-webserver/src/web/css/images/animated-overlay.gif b/azkaban-webserver/src/web/css/images/animated-overlay.gif
new file mode 100644
index 0000000..d441f75
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/animated-overlay.gif differ
diff --git a/azkaban-webserver/src/web/css/images/dot-icon.png b/azkaban-webserver/src/web/css/images/dot-icon.png
new file mode 100644
index 0000000..d54afd0
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/dot-icon.png differ
diff --git a/azkaban-webserver/src/web/css/images/redwarning.png b/azkaban-webserver/src/web/css/images/redwarning.png
new file mode 100644
index 0000000..08c5fe6
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/redwarning.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-bg_glass_55_fbf9ee_1x400.png b/azkaban-webserver/src/web/css/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 0000000..ad3d634
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-bg_glass_55_fbf9ee_1x400.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-bg_glass_65_ffffff_1x400.png b/azkaban-webserver/src/web/css/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 0000000..42ccba2
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-bg_glass_65_ffffff_1x400.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-bg_glass_75_dadada_1x400.png b/azkaban-webserver/src/web/css/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 0000000..5a46b47
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-bg_glass_75_dadada_1x400.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-bg_glass_75_e6e6e6_1x400.png b/azkaban-webserver/src/web/css/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 0000000..86c2baa
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-bg_glass_75_e6e6e6_1x400.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/azkaban-webserver/src/web/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 0000000..7c9fa6c
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_000000_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_000000_256x240.png
new file mode 100644
index 0000000..f07448d
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_000000_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_222222_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_222222_256x240.png
new file mode 100644
index 0000000..b273ff1
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_222222_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_2e83ff_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 0000000..84defe6
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_2e83ff_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_3383bb_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_3383bb_256x240.png
new file mode 100644
index 0000000..c2eb45b
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_3383bb_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_454545_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_454545_256x240.png
new file mode 100644
index 0000000..59bd45b
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_454545_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_70b2e1_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_70b2e1_256x240.png
new file mode 100644
index 0000000..66f4f00
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_70b2e1_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_888888_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_888888_256x240.png
new file mode 100644
index 0000000..6d02426
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_888888_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_999999_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_999999_256x240.png
new file mode 100644
index 0000000..da7e727
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_999999_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_cccccc_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_cccccc_256x240.png
new file mode 100644
index 0000000..9254e05
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_cccccc_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_cd0a0a_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 0000000..2ab019b
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_cd0a0a_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_fbc856_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_fbc856_256x240.png
new file mode 100644
index 0000000..69480ef
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_fbc856_256x240.png differ
diff --git a/azkaban-webserver/src/web/css/images/ui-icons_ffffff_256x240.png b/azkaban-webserver/src/web/css/images/ui-icons_ffffff_256x240.png
new file mode 100644
index 0000000..4f624bb
Binary files /dev/null and b/azkaban-webserver/src/web/css/images/ui-icons_ffffff_256x240.png differ
azkaban-webserver/src/web/css/jquery.svg.css 15(+15 -0)
diff --git a/azkaban-webserver/src/web/css/jquery.svg.css b/azkaban-webserver/src/web/css/jquery.svg.css
new file mode 100644
index 0000000..1ff48b1
--- /dev/null
+++ b/azkaban-webserver/src/web/css/jquery.svg.css
@@ -0,0 +1,15 @@
+/* http://keith-wood.name/svg.html
+ SVG for jQuery v1.4.5.
+ Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+
+svg\:svg {
+ display: none;
+}
+
+.svg_error {
+ color: red;
+ font-weight: bold;
+}
diff --git a/azkaban-webserver/src/web/css/jquery-ui-timepicker-addon.css b/azkaban-webserver/src/web/css/jquery-ui-timepicker-addon.css
new file mode 100644
index 0000000..becbd7b
--- /dev/null
+++ b/azkaban-webserver/src/web/css/jquery-ui-timepicker-addon.css
@@ -0,0 +1,10 @@
+.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
+.ui-timepicker-div dl { text-align: left; }
+.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
+.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
+.ui-timepicker-div td { font-size: 90%; }
+.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
+
+.ui-timepicker-rtl{ direction: rtl; }
+.ui-timepicker-rtl dl { text-align: right; }
+.ui-timepicker-rtl dl dd { margin: 0 65px 10px 10px; }
azkaban-webserver/src/web/css/morris.css 25(+25 -0)
diff --git a/azkaban-webserver/src/web/css/morris.css b/azkaban-webserver/src/web/css/morris.css
new file mode 100644
index 0000000..5b313c2
--- /dev/null
+++ b/azkaban-webserver/src/web/css/morris.css
@@ -0,0 +1,25 @@
+.morris-hover {
+ position: absolute;
+ z-index: 1000;
+}
+
+.morris-hover.morris-default-style {
+ border-radius: 10px;
+ padding: 6px;
+ color: #666;
+ background: rgba(255, 255, 255, 0.8);
+ border: solid 2px rgba(230, 230, 230, 0.8);
+ font-family: sans-serif;
+ font-size: 12px;
+ text-align: center;
+}
+
+.morris-hover.morris-default-style .morris-hover-row-label {
+ font-weight: bold;
+ margin: 0.25em 0;
+}
+
+.morris-hover.morris-default-style .morris-hover-point {
+ white-space: nowrap;
+ margin: 0.1em 0;
+}
diff --git a/azkaban-webserver/src/web/favicon.ico b/azkaban-webserver/src/web/favicon.ico
new file mode 100644
index 0000000..0d070bc
Binary files /dev/null and b/azkaban-webserver/src/web/favicon.ico differ
diff --git a/azkaban-webserver/src/web/images/graph-icon.png b/azkaban-webserver/src/web/images/graph-icon.png
new file mode 100644
index 0000000..d315d97
Binary files /dev/null and b/azkaban-webserver/src/web/images/graph-icon.png differ
diff --git a/azkaban-webserver/src/web/js/azkaban/model/job-log.js b/azkaban-webserver/src/web/js/azkaban/model/job-log.js
new file mode 100644
index 0000000..681875b
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/model/job-log.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.JobLogModel = Backbone.Model.extend({
+ initialize: function() {
+ this.set("offset", 0);
+ this.set("logData", "");
+ },
+
+ refresh: function() {
+ var requestURL = contextURL + "/executor";
+ var finished = false;
+
+ var date = new Date();
+ var startTime = date.getTime();
+
+ while (!finished) {
+ var requestData = {
+ "execid": execId,
+ "jobId": jobId,
+ "ajax":"fetchExecJobLogs",
+ "offset": this.get("offset"),
+ "length": 50000,
+ "attempt": attempt
+ };
+
+ var self = this;
+
+ var successHandler = function(data) {
+ console.log("fetchLogs");
+ if (data.error) {
+ console.log(data.error);
+ finished = true;
+ }
+ else if (data.length == 0) {
+ finished = true;
+ }
+ else {
+ var date = new Date();
+ var endTime = date.getTime();
+ if ((endTime - startTime) > 10000) {
+ finished = true;
+ showDialog("Alert", "The log is taking a long time to finish loading. Azkaban has stopped loading them. Please click Refresh to restart the load.");
+ }
+
+ self.set("offset", data.offset + data.length);
+ self.set("logData", self.get("logData") + data.data);
+ }
+ }
+
+ $.ajax({
+ url: requestURL,
+ type: "get",
+ async: false,
+ data: requestData,
+ dataType: "json",
+ error: function(data) {
+ console.log(data);
+ finished = true;
+ },
+ success: successHandler
+ });
+ }
+ },
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/model/svg-graph.js b/azkaban-webserver/src/web/js/azkaban/model/svg-graph.js
new file mode 100644
index 0000000..580b747
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/model/svg-graph.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2014 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.GraphModel = Backbone.Model.extend({
+ initialize: function() {
+
+ },
+
+ /*
+ * Process and add data from JSON.
+ */
+ addFlow: function(data) {
+ this.processFlowData(data);
+ this.set({'data': data});
+ },
+
+ processFlowData: function(data) {
+ var nodes = {};
+ var edges = new Array();
+
+ // Create a node map
+ for (var i = 0; i < data.nodes.length; ++i) {
+ var node = data.nodes[i];
+ nodes[node.id] = node;
+ if (!node.status) {
+ node.status = "READY";
+ }
+ }
+
+ // Create each node in and out nodes. Create an edge list.
+ for (var i = 0; i < data.nodes.length; ++i) {
+ var node = data.nodes[i];
+ if (node.in) {
+ for (var j = 0; j < node.in.length; ++j) {
+ var fromNode = nodes[node.in[j]];
+ if (!fromNode.outNodes) {
+ fromNode.outNodes = {};
+ }
+ if (!node.inNodes) {
+ node.inNodes = {};
+ }
+
+ fromNode.outNodes[node.id] = node;
+ node.inNodes[fromNode.id] = fromNode;
+ edges.push({to: node.id, from: fromNode.id});
+ }
+ }
+ }
+
+ // Iterate over the nodes again. Parse the data if they're embedded flow data.
+ // Assign each nodes to the parent flow data.
+ for (var key in nodes) {
+ var node = nodes[key];
+ node.parent = data;
+ if (node.type == "flow") {
+ this.processFlowData(node);
+ }
+ }
+
+ // Assign the node map and the edge list
+ data.nodeMap = nodes;
+ data.edges = edges;
+ }
+});
azkaban-webserver/src/web/js/azkaban/util/ajax.js 206(+206 -0)
diff --git a/azkaban-webserver/src/web/js/azkaban/util/ajax.js b/azkaban-webserver/src/web/js/azkaban/util/ajax.js
new file mode 100644
index 0000000..1194d85
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/ajax.js
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+
+function ajaxCall(requestURL, data, callback) {
+ var successHandler = function(data) {
+ if (data.error == "session") {
+ // We need to relogin.
+ var errorDialog = document.getElementById("invalid-session");
+ if (errorDialog) {
+ $(errorDialog).modal({
+ closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+ position: ["20%",],
+ containerId: 'confirm-container',
+ containerCss: {
+ 'height': '220px',
+ 'width': '565px'
+ },
+ onClose: function (dialog) {
+ window.location.reload();
+ }
+ });
+ }
+ }
+ else {
+ callback.call(this,data);
+ }
+ };
+ $.get(requestURL, data, successHandler, "json");
+}
+
+function executeFlow(executingData) {
+ executeURL = contextURL + "/executor";
+ var successHandler = function(data) {
+ if (data.error) {
+ flowExecuteDialogView.hideExecutionOptionPanel();
+ messageDialogView.show("Error Executing Flow", data.error);
+ }
+ else {
+ flowExecuteDialogView.hideExecutionOptionPanel();
+ messageDialogView.show("Flow submitted", data.message,
+ function() {
+ var redirectURL = contextURL + "/executor?execid=" + data.execid;
+ window.location.href = redirectURL;
+ }
+ );
+ }
+ };
+
+ $.get(executeURL, executingData, successHandler, "json");
+}
+
+function fetchFlowInfo(model, projectName, flowId, execId) {
+ var fetchData = {"project": projectName, "ajax":"flowInfo", "flow":flowId};
+ if (execId) {
+ fetchData.execid = execId;
+ }
+
+ var executeURL = contextURL + "/executor";
+ var successHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ }
+ else {
+ model.set({
+ "successEmails": data.successEmails,
+ "failureEmails": data.failureEmails,
+ "failureAction": data.failureAction,
+ "notifyFailure": {
+ "first": data.notifyFailureFirst,
+ "last": data.notifyFailureLast
+ },
+ "flowParams": data.flowParam,
+ "isRunning": data.running,
+ "nodeStatus": data.nodeStatus,
+ "concurrentOption": data.concurrentOptions,
+ "pipelineLevel": data.pipelineLevel,
+ "pipelineExecution": data.pipelineExecution,
+ "queueLevel":data.queueLevel
+ });
+ }
+ model.trigger("change:flowinfo");
+ };
+
+ $.ajax({
+ url: executeURL,
+ data: fetchData,
+ success: successHandler,
+ dataType: "json",
+ async: false
+ });
+}
+
+function fetchFlow(model, projectName, flowId, sync) {
+ // Just in case people don't set sync
+ sync = sync ? true : false;
+ var managerUrl = contextURL + "/manager";
+ var fetchData = {
+ "ajax" : "fetchflowgraph",
+ "project" : projectName,
+ "flow" : flowId
+ };
+ var successHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ }
+ else {
+ var disabled = data.disabled ? data.disabled : {};
+ model.set({
+ flowId: data.flowId,
+ data: data,
+ disabled: disabled
+ });
+
+ var nodeMap = {};
+ for (var i = 0; i < data.nodes.length; ++i) {
+ var node = data.nodes[i];
+ nodeMap[node.id] = node;
+ }
+
+ for (var i = 0; i < data.edges.length; ++i) {
+ var edge = data.edges[i];
+
+ if (!nodeMap[edge.target].in) {
+ nodeMap[edge.target].in = {};
+ }
+ var targetInMap = nodeMap[edge.target].in;
+ targetInMap[edge.from] = nodeMap[edge.from];
+
+ if (!nodeMap[edge.from].out) {
+ nodeMap[edge.from].out = {};
+ }
+ var sourceOutMap = nodeMap[edge.from].out;
+ sourceOutMap[edge.target] = nodeMap[edge.target];
+ }
+
+ model.set({nodeMap: nodeMap});
+ }
+ };
+
+ $.ajax({
+ url: managerUrl,
+ data: fetchData,
+ success: successHandler,
+ dataType: "json",
+ async: !sync
+ });
+}
+
+/**
+* Checks to see if a flow is running.
+*
+*/
+function flowExecutingStatus(projectName, flowId) {
+ var requestURL = contextURL + "/executor";
+
+ var executionIds;
+ var successHandler = function(data) {
+ if (data.error == "session") {
+ // We need to relogin.
+ var errorDialog = document.getElementById("invalid-session");
+ if (errorDialog) {
+ $(errorDialog).modal({
+ closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+ position: ["20%",],
+ containerId: 'confirm-container',
+ containerCss: {
+ 'height': '220px',
+ 'width': '565px'
+ },
+ onClose: function (dialog) {
+ window.location.reload();
+ }
+ });
+ }
+ }
+ else {
+ executionIds = data.execIds;
+ }
+ };
+ $.ajax({
+ url: requestURL,
+ async: false,
+ data: {
+ "ajax": "getRunning",
+ "project": projectName,
+ "flow": flowId
+ },
+ error: function(data) {},
+ success: successHandler
+ });
+
+ return executionIds;
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/util/common.js b/azkaban-webserver/src/web/js/azkaban/util/common.js
new file mode 100644
index 0000000..a295218
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/common.js
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+function addClass(el, name) {
+ if (!hasClass(el, name)) {
+ var classes = el.getAttribute("class");
+ classes += classes ? ' ' + name : '' +name;
+ el.setAttribute("class", classes);
+ }
+}
+
+function removeClass(el, name) {
+ if (hasClass(el, name)) {
+ var classes = el.getAttribute("class");
+ el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
+ }
+}
+
+function hasClass(el, name) {
+ var classes = el.getAttribute("class");
+ if (classes == null) {
+ return false;
+ }
+ return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
+}
+
+function sizeStrToBytes(str) {
+ if (str.length == 0) {
+ return 0;
+ }
+ var unit = str.charAt(str.length - 1)
+ if (!isNaN(unit)) {
+ return parseInt(str);
+ }
+ var val = parseInt(str.substring(0, str.length - 1));
+ unit = unit.toUpperCase();
+ if (unit == 'M') {
+ val *= 0x100000;
+ }
+ else if (unit == 'G') {
+ val *= 0x40000000;
+ }
+ return val;
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/util/date.js b/azkaban-webserver/src/web/js/azkaban/util/date.js
new file mode 100644
index 0000000..c7dca87
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/date.js
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+var TIMESTAMP_LENGTH = 13;
+
+var getDuration = function(startMs, endMs) {
+ if (startMs) {
+ if (endMs == null || endMs < startMs) {
+ return "-";
+ }
+
+ var diff = endMs - startMs;
+ return formatDuration(diff, false);
+ }
+
+ return "-";
+}
+
+var formatDuration = function(duration, millisecSig) {
+ var diff = duration;
+ var seconds = Math.floor(diff / 1000);
+
+ if (seconds < 60) {
+ if (millisecSig) {
+ return (diff / 1000).toFixed(millisecSig) + " s";
+ }
+ else {
+ return seconds + " sec";
+ }
+ }
+
+ var mins = Math.floor(seconds / 60);
+ seconds = seconds % 60;
+ if (mins < 60) {
+ return mins + "m " + seconds + "s";
+ }
+
+ var hours = Math.floor(mins / 60);
+ mins = mins % 60;
+ if (hours < 24) {
+ return hours + "h " + mins + "m " + seconds + "s";
+ }
+
+ var days = Math.floor(hours / 24);
+ hours = hours % 24;
+
+ return days + "d " + hours + "h " + mins + "m";
+}
+
+var getDateFormat = function(date) {
+ var year = date.getFullYear();
+ var month = getTwoDigitStr(date.getMonth() + 1);
+ var day = getTwoDigitStr(date.getDate());
+
+ var hours = getTwoDigitStr(date.getHours());
+ var minutes = getTwoDigitStr(date.getMinutes());
+ var second = getTwoDigitStr(date.getSeconds());
+
+ var datestring = year + "-" + month + "-" + day + " " + hours + ":" +
+ minutes + " " + second + "s";
+ return datestring;
+}
+
+var getHourMinSec = function(date) {
+ var hours = getTwoDigitStr(date.getHours());
+ var minutes = getTwoDigitStr(date.getMinutes());
+ var second = getTwoDigitStr(date.getSeconds());
+
+ var timestring = hours + ":" + minutes + " " + second + "s";
+ return timestring;
+}
+
+var getTwoDigitStr = function(value) {
+ if (value < 10) {
+ return "0" + value;
+ }
+
+ return value;
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/util/flow-loader.js b/azkaban-webserver/src/web/js/azkaban/util/flow-loader.js
new file mode 100644
index 0000000..015f02a
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/flow-loader.js
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2014 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.
+ */
+
+var extendedViewPanels = {};
+var extendedDataModels = {};
+var openJobDisplayCallback = function(nodeId, flowId, evt) {
+ console.log("Open up data");
+
+ /*
+ $("#flowInfoBase").before(cloneStuff);
+ var requestURL = contextURL + "/manager";
+
+ $.get(
+ requestURL,
+ {"project": projectName, "ajax":"fetchflownodedata", "flow":flowId, "node": nodeId},
+ function(data) {
+ var graphModel = new azkaban.GraphModel();
+ graphModel.set({id: data.id, flow: data.flowData, type: data.type, props: data.props});
+
+ var flowData = data.flowData;
+ if (flowData) {
+ createModelFromAjaxCall(flowData, graphModel);
+ }
+
+ var backboneView = new azkaban.FlowExtendedViewPanel({el:cloneStuff, model: graphModel});
+ extendedViewPanels[nodeInfoPanelID] = backboneView;
+ extendedDataModels[nodeInfoPanelID] = graphModel;
+ backboneView.showExtendedView(evt);
+ },
+ "json"
+ );
+ */
+}
+
+var createNewPanel = function(node, model, evt) {
+ var parentPath = node.parentPath;
+
+ var nodeInfoPanelID = parentPath ? parentPath + ":" + node.id + "-info" : node.id + "-info";
+ var cloneStuff = $("#flowInfoBase").clone();
+ cloneStuff.data = node;
+ $(cloneStuff).attr("id", nodeInfoPanelID);
+ $("#flowInfoBase").before(cloneStuff);
+
+ var backboneView = new azkaban.FlowExtendedViewPanel({el:cloneStuff, model: model});
+ node.panel = backboneView;
+ backboneView.showExtendedView(evt);
+}
+
+var closeAllSubDisplays = function() {
+ $(".flowExtendedView").hide();
+}
+
+var nodeClickCallback = function(event, model, node) {
+ console.log("Node clicked callback");
+
+ var target = event.currentTarget;
+ var type = node.type;
+ var flowId = node.parent.flow;
+ var jobId = node.id;
+
+ var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+ var menu = [];
+
+ if (type == "flow") {
+ var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + node.flowId;
+ if (node.expanded) {
+ menu = [{title: "Collapse Flow...", callback: function() {model.trigger("collapseFlow", node);}}];
+ }
+ else {
+ menu = [{title: "Expand Flow...", callback: function() {model.trigger("expandFlow", node);}}];
+ }
+
+ $.merge(menu, [
+ // {title: "View Properties...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+ {break: 1},
+ {title: "Open Flow...", callback: function() {window.location.href=flowRequestURL;}},
+ {title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}},
+ {break: 1},
+ {title: "Open Properties...", callback: function() {window.location.href=requestURL;}},
+ {title: "Open Properties in New Window...", callback: function() {window.open(requestURL);}},
+ {break: 1},
+ {title: "Center Flow", callback: function() {model.trigger("centerNode", node);}}
+ ]);
+ }
+ else {
+ menu = [
+ // {title: "View Properties...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+ // {break: 1},
+ {title: "Open Job...", callback: function() {window.location.href=requestURL;}},
+ {title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+ {break: 1},
+ {title: "Center Job", callback: function() {model.trigger("centerNode", node)}}
+ ];
+ }
+ contextMenuView.show(event, menu);
+}
+
+var jobClickCallback = function(event, model, node) {
+ console.log("Node clicked callback");
+ var target = event.currentTarget;
+ var type = node.type;
+ var flowId = node.parent.flow;
+ var jobId = node.id;
+
+ var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + node.id;
+
+ var menu;
+ if (type == "flow") {
+ var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + node.flowId;
+ menu = [
+ // {title: "View Properties...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+ // {break: 1},
+ {title: "Open Flow...", callback: function() {window.location.href=flowRequestURL;}},
+ {title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}},
+ {break: 1},
+ {title: "Open Properties...", callback: function() {window.location.href=requestURL;}},
+ {title: "Open Properties in New Window...", callback: function() {window.open(requestURL);}},
+ {break: 1},
+ {title: "Center Flow", callback: function() {model.trigger("centerNode", node)}}
+ ];
+ }
+ else {
+ menu = [
+ // {title: "View Job...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+ // {break: 1},
+ {title: "Open Job...", callback: function() {window.location.href=requestURL;}},
+ {title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+ {break: 1},
+ {title: "Center Job", callback: function() {graphModel.trigger("centerNode", node)}}
+ ];
+ }
+ contextMenuView.show(event, menu);
+}
+
+var edgeClickCallback = function(event, model) {
+ console.log("Edge clicked callback");
+}
+
+var graphClickCallback = function(event, model) {
+ console.log("Graph clicked callback");
+ var data = model.get("data");
+ var flowId = data.flow;
+ var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
+
+ var menu = [
+ {title: "Open Flow...", callback: function() {window.location.href=requestURL;}},
+ {title: "Open Flow in New Window...", callback: function() {window.open(requestURL);}},
+ {break: 1},
+ {title: "Center Graph", callback: function() {model.trigger("resetPanZoom");}}
+ ];
+
+ contextMenuView.show(event, menu);
+}
+
diff --git a/azkaban-webserver/src/web/js/azkaban/util/job-status.js b/azkaban-webserver/src/web/js/azkaban/util/job-status.js
new file mode 100644
index 0000000..fd61693
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/job-status.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+var statusList = ["FAILED", "FAILED_FINISHING", "SUCCEEDED", "RUNNING", "WAITING", "KILLED", "DISABLED", "READY", "CANCELLED", "UNKNOWN", "PAUSED", "SKIPPED", "QUEUED"];
+var statusStringMap = {
+ "QUEUED": "Queued",
+ "SKIPPED": "Skipped",
+ "PREPARING": "Preparing",
+ "FAILED": "Failed",
+ "SUCCEEDED": "Success",
+ "FAILED_FINISHING": "Running w/Failure",
+ "RUNNING": "Running",
+ "WAITING": "Waiting",
+ "KILLED": "Killed",
+ "CANCELLED": "Cancelled",
+ "DISABLED": "Disabled",
+ "READY": "Ready",
+ "UNKNOWN": "Unknown",
+ "PAUSED": "Paused"
+};
azkaban-webserver/src/web/js/azkaban/util/layout.js 384(+384 -0)
diff --git a/azkaban-webserver/src/web/js/azkaban/util/layout.js b/azkaban-webserver/src/web/js/azkaban/util/layout.js
new file mode 100644
index 0000000..acdb832
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/layout.js
@@ -0,0 +1,384 @@
+/*
+ * 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.
+ */
+
+var maxTextSize = 32;
+var reductionSize = 26;
+var degreeRatio = 1/8;
+var maxHeight = 200;
+var cornerGap = 10;
+
+var idSort = function(a, b) {
+ if ( a.id < b.id ) {
+ return -1;
+ }
+ else if ( a.id > b.id ) {
+ return 1;
+ }
+ else {
+ return 0;
+ }
+}
+
+function prepareLayout(nodes, hmargin, layers, nodeMap) {
+ var maxLayer = 0;
+ var nodeQueue = new Array();
+ // Find start layers first
+ for (var i=0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ if (node.inNodes) {
+ // We sort here. Why? To keep the node drawing consistent
+ node.in.sort(idSort);
+ }
+ else {
+ // We sort here. Why? To keep it up and running.
+ nodeQueue.push(node);
+ }
+ }
+ // Sort here. To keep the node drawing consistent
+ nodes.sort(idSort);
+
+ // calculate level
+ // breath first search the sucker
+ var index = 0;
+ while(index < nodeQueue.length) {
+ var node = nodeQueue[index];
+ if (node.inNodes) {
+ var level = 0;
+ for (var key in node.inNodes) {
+ level = Math.max(level, node.inNodes[key].level);
+ }
+ node.level = level + 1;
+ }
+ else {
+ node.level = 0;
+ }
+
+ if (node.outNodes) {
+ for (var key in node.outNodes) {
+ nodeQueue.push(node.outNodes[key]);
+ }
+ }
+ index++;
+ }
+
+ // Assign to layers
+ for (var i = 0; i < nodes.length; ++i) {
+ var width = nodes[i].width ? nodes[i].width : nodes[i].label.length * 11.5 + 4;
+ var height = nodes[i].height ? nodes[i].height : 1;
+ var node = { id: nodes[i].id, node: nodes[i], level: nodes[i].level, in:[], out:[], width: width + hmargin, x:0, height:height };
+ nodeMap[nodes[i].id] = node;
+ maxLayer = Math.max(node.level, maxLayer);
+ if(!layers[node.level]) {
+ layers[node.level] = [];
+ }
+
+ layers[node.level].push(node);
+ }
+
+ layers.maxLayer = maxLayer;
+}
+
+function respaceGraph(nodes, edges) {
+
+}
+
+function layoutGraph(nodes, edges, hmargin) {
+ var startLayer = [];
+
+ var nodeMap = {};
+ var layers = {};
+
+ if (!hmargin) {
+ hmargin = 8;
+ }
+
+ prepareLayout(nodes, hmargin, layers, nodeMap);
+ var maxLayer = layers.maxLayer;
+
+ // Create dummy nodes
+ var edgeDummies = {};
+ for (var i=0; i < edges.length; ++i ) {
+ var edge = edges[i];
+ var src = edges[i].from;
+ var dest = edges[i].to;
+
+ var edgeId = src + ">>" + dest;
+
+ var srcNode = nodeMap[src];
+ var destNode = nodeMap[dest];
+
+ var lastNode = srcNode;
+
+ var guides = [];
+
+ for (var j = srcNode.level + 1; j < destNode.level; ++j) {
+ var dummyNode = {level: j, in: [], x: lastNode.x, out: [], realSrc: srcNode, realDest: destNode, width: 10, height: 10};
+ layers[j].push(dummyNode);
+ dummyNode.in.push(lastNode);
+ lastNode.out.push(dummyNode);
+ lastNode = dummyNode;
+
+ guides.push(dummyNode);
+ }
+
+ destNode.in.push(lastNode);
+ lastNode.out.push(destNode);
+
+ if (edgeDummies.length != 0) {
+ edgeDummies[edgeId] = guides;
+ }
+ }
+
+ spreadLayerSmart(layers[maxLayer]);
+ sort(layers[maxLayer]);
+ for (var i=maxLayer - 1; i >=0; --i) {
+ uncrossWithOut(layers[i]);
+ sort(layers[i]);
+
+ spreadLayerSmart(layers[i]);
+ }
+
+ // The top level can get out of alignment, so we do this kick back
+ // manouver before we seriously get started sorting.
+ if (maxLayer > 1) {
+ uncrossWithIn(layers[1]);
+ sort(layers[1]);
+ spreadLayerSmart(layers[1]);
+
+ uncrossWithOut(layers[0]);
+ sort(layers[0]);
+ spreadLayerSmart(layers[0]);
+ }
+
+ // Uncross down
+ for (var i=1; i <= maxLayer; ++i) {
+ uncrossWithIn(layers[i]);
+ sort(layers[i]);
+ spreadLayerSmart(layers[i]);
+ }
+
+ // Space it vertically
+ spaceVertically(layers, maxLayer);
+
+ // Assign points to nodes
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+ var layerNode = nodeMap[node.id];
+ node.x = layerNode.x;
+ node.y = layerNode.y;
+ }
+
+ // Dummy node for more points.
+ for (var i = 0; i < edges.length; ++i) {
+ var edge = edges[i];
+ var src = edges[i].from;
+ var dest = edges[i].to;
+
+ var edgeId = src + ">>" + dest;
+ if (edgeDummies[edgeId] && edgeDummies[edgeId].length > 0) {
+ var prevX = nodeMap[src].x;
+ var destX = nodeMap[dest].x;
+
+ var guides = [];
+ var dummies = edgeDummies[edgeId];
+ for (var j=0; j< dummies.length; ++j) {
+ var point = {x: dummies[j].x, y: dummies[j].y};
+ guides.push(point);
+
+ var nextX = j == dummies.length - 1 ? destX: dummies[j + 1].x;
+ if (point.x != prevX && point.x != nextX) {
+ // Add gap
+ if ((point.x > prevX) == (point.x > nextX)) {
+ guides.push({x: point.x, y:point.y + cornerGap});
+ }
+ }
+ prevX = point.x;
+ }
+
+ edge.guides = guides;
+ }
+ else {
+ edge.guides = null;
+ }
+ }
+}
+
+function spreadLayerSmart(layer) {
+ var ranges = [];
+ ranges.push({
+ start: 0,
+ end: 0,
+ width: layer[0].width,
+ x: layer[0].x,
+ index: 0
+ });
+ var largestRangeIndex = -1;
+
+ var totalX = layer[0].x;
+ var totalWidth = layer[0].width;
+ var count = 1;
+
+ for (var i = 1; i < layer.length; ++i ) {
+ var prevRange = ranges[ranges.length - 1];
+ var delta = layer[i].x - prevRange.x;
+
+ if (delta == 0) {
+ prevRange.end = i;
+ prevRange.width += layer[i].width;
+ totalWidth += layer[i].width;
+ }
+ else {
+ totalWidth += Math.max(layer[i].width, delta);
+ ranges.push({
+ start: i,
+ end: i,
+ width: layer[i].width,
+ x: layer[i].x,
+ index: ranges.length
+ });
+ }
+
+ totalX += layer[i].x;
+ count++;
+ }
+
+ // Space the ranges, but place the left and right most last
+ var startIndex = 0;
+ var endIndex = 0;
+ if (ranges.length == 1) {
+ startIndex = -1;
+ endIndex = 1;
+ }
+ else if ((ranges.length % 2) == 1) {
+ var index = Math.ceil(ranges.length/2);
+ startIndex = index - 1;
+ endIndex = index + 1;
+ }
+ else {
+ var e = ranges.length/2;
+ var s = e - 1;
+
+ var crossPointS = ranges[s].x + ranges[s].width/2;
+ var crossPointE = ranges[e].x - ranges[e].width/2;
+
+ if (crossPointS > crossPointE) {
+ var midPoint = (ranges[s].x + ranges[e].x)/2;
+ ranges[s].x = midPoint - ranges[s].width/2;
+ ranges[e].x = midPoint + ranges[e].width/2;
+ }
+
+ startIndex = s - 1;
+ endIndex = e + 1;
+ }
+
+ for (var i = startIndex; i >= 0; --i) {
+ var range = ranges[i];
+ var crossPointS = range.x + range.width/2;
+ var crossPointE = ranges[i + 1].x - ranges[i + 1].width/2;
+ if (crossPointE < crossPointS) {
+ range.x -= crossPointS - crossPointE;
+ }
+ }
+
+ for (var i = endIndex; i < ranges.length; ++i) {
+ var range = ranges[i];
+ var crossPointE = range.x - range.width/2;
+ var crossPointS = ranges[i - 1].x + ranges[i - 1].width/2;
+ if (crossPointE < crossPointS) {
+ range.x += crossPointS - crossPointE;
+ }
+ }
+
+ for (var i = 0; i < ranges.length; ++i) {
+ var range = ranges[i];
+ if (range.start == range.end) {
+ layer[range.start].x = range.x;
+ }
+ else {
+ var start = range.x - range.width/2;
+ for (var j=range.start;j <=range.end; ++j) {
+ layer[j].x = start + layer[j].width/2;
+ start += layer[j].width;
+ }
+ }
+ }
+}
+
+function spaceVertically(layers, maxLayer) {
+ var startY = 0;
+ var startLayer = layers[0];
+ var startMaxHeight = 1;
+ for (var i=0; i < startLayer.length; ++i) {
+ startLayer[i].y = startY;
+ startMaxHeight = Math.max(startMaxHeight, startLayer[i].height);
+ }
+
+ var minHeight = 40;
+ for (var a=1; a <= maxLayer; ++a) {
+ var maxDelta = 0;
+ var layer = layers[a];
+
+ var layerMaxHeight = 1;
+ for (var i=0; i < layer.length; ++i) {
+ layerMaxHeight = Math.max(layerMaxHeight, layer[i].height);
+
+ for (var j=0; j < layer[i].in.length; ++j) {
+ var upper = layer[i].in[j];
+ var delta = Math.abs(upper.x - layer[i].x);
+ maxDelta = Math.max(maxDelta, delta);
+ }
+ }
+
+ console.log("Max " + maxDelta);
+ var calcHeight = maxDelta*degreeRatio;
+
+ var newMinHeight = minHeight + startMaxHeight/2 + layerMaxHeight / 2;
+ startMaxHeight = layerMaxHeight;
+
+ startY += Math.max(calcHeight, newMinHeight);
+ for (var i=0; i < layer.length; ++i) {
+ layer[i].y=startY;
+ }
+ }
+}
+
+function uncrossWithIn(layer) {
+ for (var i = 0; i < layer.length; ++i) {
+ var pos = findAverage(layer[i].in);
+ layer[i].x = pos;
+ }
+}
+
+function findAverage(nodes) {
+ var sum = 0;
+ for (var i = 0; i < nodes.length; ++i) {
+ sum += nodes[i].x;
+ }
+ return sum/nodes.length;
+}
+
+function uncrossWithOut(layer) {
+ for (var i = 0; i < layer.length; ++i) {
+ var pos = findAverage(layer[i].out);
+ layer[i].x = pos;
+ }
+}
+
+function sort(layer) {
+ layer.sort(function(a, b) {
+ return a.x - b.x;
+ });
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/util/schedule.js b/azkaban-webserver/src/web/js/azkaban/util/schedule.js
new file mode 100644
index 0000000..de3da39
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/schedule.js
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+function removeSched(scheduleId) {
+ var scheduleURL = contextURL + "/schedule"
+ var redirectURL = contextURL + "/schedule"
+ var requestData = {
+ "action": "removeSched",
+ "scheduleId":scheduleId
+ };
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#errorMsg').text(data.error);
+ }
+ else {
+ window.location = redirectURL;
+ }
+ };
+ $.post(scheduleURL, requestData, successHandler, "json");
+}
+
+function removeSla(scheduleId) {
+ var scheduleURL = contextURL + "/schedule"
+ var redirectURL = contextURL + "/schedule"
+ var requestData = {
+ "action": "removeSla",
+ "scheduleId": scheduleId
+ };
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#errorMsg').text(data.error)
+ }
+ else {
+ window.location = redirectURL
+ }
+ };
+ $.post(scheduleURL, requestData, successHandler, "json");
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/util/svg-navigate.js b/azkaban-webserver/src/web/js/azkaban/util/svg-navigate.js
new file mode 100644
index 0000000..0d11d6e
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/svg-navigate.js
@@ -0,0 +1,407 @@
+/*
+ * 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.
+ */
+
+(function($) {
+ var mouseUp = function(evt) {
+ if (evt.button > 1) {
+ return;
+ }
+ var target = evt.target;
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ target.mDown = false;
+ }
+
+ var mouseDown = function(evt) {
+ if (evt.button > 1) {
+ return;
+ }
+
+ var target = evt.target;
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ target.mDown = true;
+ }
+
+ var mouseOut = function(evt) {
+ var target = evt.target;
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ target.mDown = false;
+ }
+
+ var mouseMove = function(evt) {
+ var target = evt.target;
+ if (target.mDown) {
+ var dx = evt.clientX - target.mx;
+ var dy = evt.clientY - target.my;
+
+ evt.dragX = dx;
+ evt.dragY = dy;
+ mouseDrag(evt);
+ }
+
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ }
+
+ var mouseDrag = function(evt) {
+ translateDeltaGraph(evt.target, evt.dragX, evt.dragY);
+ }
+
+ var mouseScrolled = function(evt) {
+ if (!evt) {
+ evt = window.event;
+ }
+ var target = evt.currentTarget;
+
+ var leftOffset = 0;
+ var topOffset = 0;
+ if (!target.marker) {
+ while (!target.farthestViewportElement) {
+ target = target.parentNode;
+ }
+
+ target = target.farthestViewportElement;
+ }
+
+ // Trackball/trackpad vs wheel. Need to accommodate
+ var delta = 0;
+ if (evt.wheelDelta) {
+ if (evt.wheelDelta > 0) {
+ delta = Math.ceil(evt.wheelDelta / 120);
+ }
+ else {
+ delta = Math.floor(evt.wheelDelta / 120);
+ }
+ }
+ else if (evt.detail) {
+ if (evt.detail > 0) {
+ delta = -Math.ceil(evt.detail / 3);
+ }
+ else {
+ delta = -Math.floor(evt.detail / 3);
+ }
+ }
+
+ var zoomLevel = boundZoomLevel(target, target.zoomIndex + delta);
+ target.zoomIndex = zoomLevel;
+ var scale = target.zoomLevels[zoomLevel];
+
+ var x = evt.offsetX;
+ var y = evt.offsetY;
+ if (!x) {
+ var position = $(target.parentElement).position();
+ x = evt.layerX - position.left;
+ y = evt.layerY - position.top;
+ }
+
+ evt.stopPropagation();
+ evt.preventDefault();
+
+ scaleGraph(target, scale, x, y);
+ }
+
+ this.boundZoomLevel = function(target, level) {
+ if (level >= target.settings.zoomNumLevels) {
+ return target.settings.zoomNumLevels - 1;
+ }
+ else if (level <= 0) {
+ return 0;
+ }
+
+ return level;
+ }
+
+ this.scaleGraph = function(target, scale, x, y) {
+ var sfactor = scale / target.scale;
+ target.scale = scale;
+
+ target.translateX = sfactor * target.translateX + x - sfactor * x;
+ target.translateY = sfactor * target.translateY + y - sfactor * y;
+
+ if (target.model) {
+ target.model.trigger("scaled");
+ }
+ retransform(target);
+ }
+
+ this.translateDeltaGraph = function(target, x, y) {
+ target.translateX += x;
+ target.translateY += y;
+ if (target.model) {
+ target.model.trigger("panned");
+ }
+ retransform(target);
+ }
+
+ this.retransform = function(target) {
+ var gs = target.childNodes;
+
+ var transformString = "translate(" + target.translateX + "," + target.translateY +
+ ") scale(" + target.scale + ")";
+
+ for (var i = 0; i < gs.length; ++i) {
+ var g = gs[i];
+ if (g.nodeName == 'g') {
+ g.setAttribute("transform", transformString);
+ }
+ }
+
+ if (target.model) {
+ var obj = target.model.get("transform");
+ if (obj) {
+ obj.scale = target.scale;
+ obj.height = target.parentNode.clientHeight;
+ obj.width = target.parentNode.clientWidth;
+
+ obj.x1 = target.translateX;
+ obj.y1 = target.translateY;
+ obj.x2 = obj.x1 + obj.width * obj.scale;
+ obj.y2 = obj.y1 + obj.height * obj.scale;
+ }
+ }
+ }
+
+ this.resetTransform = function(target) {
+ var settings = target.settings;
+ target.translateX = settings.x;
+ target.translateY = settings.y;
+
+ if (settings.x < settings.x2) {
+ var factor = 0.90;
+
+ // Reset scale and stuff.
+ var divHeight = target.parentNode.clientHeight;
+ var divWidth = target.parentNode.clientWidth;
+
+ var width = settings.x2 - settings.x;
+ var height = settings.y2 - settings.y;
+ var aspectRatioGraph = height / width;
+ var aspectRatioDiv = divHeight / divWidth;
+
+ var scale = aspectRatioGraph > aspectRatioDiv
+ ? (divHeight / height) * factor
+ : (divWidth / width) * factor;
+ target.scale = scale;
+ }
+ else {
+ target.zoomIndex = boundZoomLevel(target, settings.zoomIndex);
+ target.scale = target.zoomLevels[target.zoomIndex];
+ }
+ }
+
+ this.animateTransform = function(target, scale, x, y, duration) {
+ var zoomLevel = calculateZoomLevel(scale, target.zoomLevels);
+ target.fromScaleLevel = target.zoomIndex;
+ target.toScaleLevel = zoomLevel;
+ target.fromX = target.translateX;
+ target.fromY = target.translateY;
+ target.fromScale = target.scale;
+ target.toScale = target.zoomLevels[zoomLevel];
+ target.toX = x;
+ target.toY = y;
+ target.startTime = new Date().getTime();
+ target.endTime = target.startTime + duration;
+
+ this.animateTick(target);
+ }
+
+ this.animateTick = function(target) {
+ var time = new Date().getTime();
+ if (time < target.endTime) {
+ var timeDiff = time - target.startTime;
+ var progress = timeDiff / (target.endTime - target.startTime);
+
+ target.scale = (target.toScale - target.fromScale) * progress + target.fromScale;
+ target.translateX = (target.toX - target.fromX) * progress + target.fromX;
+ target.translateY = (target.toY - target.fromY) * progress + target.fromY;
+ retransform(target);
+ setTimeout(function() {
+ this.animateTick(target)
+ }, 1);
+ }
+ else {
+ target.zoomIndex = target.toScaleLevel;
+ target.scale = target.zoomLevels[target.zoomIndex];
+ target.translateX = target.toX;
+ target.translateY = target.toY;
+ retransform(target);
+ }
+ }
+
+ this.calculateZoomScale = function(scaleLevel, numLevels, points) {
+ if (scaleLevel <= 0) {
+ return points[0];
+ }
+ else if (scaleLevel >= numLevels) {
+ return points[points.length - 1];
+ }
+ var factor = (scaleLevel / numLevels) * (points.length - 1);
+ var floorIdx = Math.floor(factor);
+ var ceilingIdx = Math.ceil(factor);
+
+ var b = factor - floorIdx;
+
+ return b * (points[ceilingIdx] - points[floorIdx]) + points[floorIdx];
+ }
+
+ this.calculateZoomLevel = function(scale, zoomLevels) {
+ if (scale >= zoomLevels[zoomLevels.length - 1]) {
+ return zoomLevels.length - 1;
+ }
+ else if (scale <= zoomLevels[0]) {
+ return 0;
+ }
+
+ var i = 0;
+ // Plain old linear scan
+ for (; i < zoomLevels.length; ++i) {
+ if (scale < zoomLevels[i]) {
+ i--;
+ break;
+ }
+ }
+
+ if (i < 0) {
+ return 0;
+ }
+
+ return i;
+ }
+
+ var methods = {
+ init : function(options) {
+ var settings = {
+ x : 0,
+ y : 0,
+ x2 : 0,
+ y2 : 0,
+ minX : -1000,
+ minY : -1000,
+ maxX : 1000,
+ maxY : 1000,
+ zoomIndex : 24,
+ zoomPoints : [ 0.1, 0.14, 0.2, 0.4, 0.8, 1, 1.6, 2.4, 4, 8, 16 ],
+ zoomNumLevels : 48
+ };
+ if (options) {
+ $.extend(settings, options);
+ }
+ return this.each(function() {
+ var $this = $(this);
+ this.settings = settings;
+ this.marker = true;
+
+ if (window.addEventListener) {
+ this.addEventListener('DOMMouseScroll', mouseScrolled,false);
+ }
+ this.onmousewheel = mouseScrolled;
+ this.onmousedown = mouseDown;
+ this.onmouseup = mouseUp;
+ this.onmousemove = mouseMove;
+ this.onmouseout = mouseOut;
+
+ this.zoomLevels = new Array(settings.zoomNumLevels);
+ for ( var i = 0; i < settings.zoomNumLevels; ++i) {
+ var scale = calculateZoomScale(i, settings.zoomNumLevels, settings.zoomPoints);
+ this.zoomLevels[i] = scale;
+ }
+ resetTransform(this);
+ });
+ },
+ transformToBox : function(arguments) {
+ var $this = $(this);
+ var target = ($this)[0];
+ var x = arguments.x;
+ var y = arguments.y;
+ var factor = 0.9;
+ var duration = arguments.duration;
+
+ var width = arguments.width ? arguments.width : 1;
+ var height = arguments.height ? arguments.height : 1;
+
+ var divHeight = target.parentNode.clientHeight;
+ var divWidth = target.parentNode.clientWidth;
+
+ var aspectRatioGraph = height / width;
+ var aspectRatioDiv = divHeight / divWidth;
+
+ var scale = aspectRatioGraph > aspectRatioDiv
+ ? (divHeight / height) * factor
+ : (divWidth / width) * factor;
+
+ if (arguments.maxScale) {
+ if (scale > arguments.maxScale) {
+ scale = arguments.maxScale;
+ }
+ }
+ if (arguments.minScale) {
+ if (scale < arguments.minScale) {
+ scale = arguments.minScale;
+ }
+ }
+
+ // Center
+ var scaledWidth = width * scale;
+ var scaledHeight = height * scale;
+
+ var sx = (divWidth - scaledWidth) / 2 - scale * x;
+ var sy = (divHeight - scaledHeight) / 2 - scale * y;
+ console.log("sx,sy:" + sx + "," + sy);
+
+ if (duration != 0 && !duration) {
+ duration = 500;
+ }
+
+ animateTransform(target, scale, sx, sy, duration);
+ },
+ attachNavigateModel : function(arguments) {
+ var $this = $(this);
+ var target = ($this)[0];
+ target.model = arguments;
+
+ if (target.model) {
+ var obj = {};
+ obj.scale = target.scale;
+ obj.height = target.parentNode.clientHeight;
+ obj.width = target.parentNode.clientWidth;
+
+ obj.x1 = target.translateX;
+ obj.y1 = target.translateY;
+ obj.x2 = obj.x1 + obj.height * obj.scale;
+ obj.y2 = obj.y1 + obj.width * obj.scale;
+
+ target.model.set({
+ transform : obj
+ });
+ }
+ }
+ };
+
+ // Main Constructor
+ $.fn.svgNavigate = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ else if (typeof method === 'object' || !method) {
+ return methods.init.apply(this, arguments);
+ }
+ else {
+ $.error('Method ' + method + ' does not exist on svgNavigate');
+ }
+ };
+})(jQuery);
diff --git a/azkaban-webserver/src/web/js/azkaban/util/svgutils.js b/azkaban-webserver/src/web/js/azkaban/util/svgutils.js
new file mode 100644
index 0000000..3802f86
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/util/svgutils.js
@@ -0,0 +1,67 @@
+function hasClass(el, name) {
+ var classes = el.getAttribute("class");
+ if (classes == null) {
+ return false;
+ }
+ return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
+}
+
+function addClass(el, name) {
+ if (!hasClass(el, name)) {
+ var classes = el.getAttribute("class");
+ if (classes) {
+ classes += ' ' + name;
+ }
+ else {
+ classes = name;
+ }
+ el.setAttribute("class", classes);
+ }
+}
+
+function removeClass(el, name) {
+ if (hasClass(el, name)) {
+ var classes = el.getAttribute("class");
+ el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
+ }
+}
+
+function translateStr(x, y) {
+ return "translate(" + x + "," + y + ")";
+}
+
+function animatePolylineEdge(svg, edge, newPoints, time) {
+ var oldEdgeGuides = edge.oldpoints;
+
+ var interval = 10;
+ var numsteps = time/interval;
+
+ var deltaEdges = new Array();
+ for (var i=0; i < oldEdgeGuides.length; ++i) {
+ var startPoint = oldEdgeGuides[i];
+ var endPoint = newPoints[i];
+
+ var deltaX = (endPoint[0] - startPoint[0])/numsteps;
+ var deltaY = (endPoint[1] - startPoint[1])/numsteps;
+ deltaEdges.push([deltaX, deltaY]);
+ }
+
+ animatePolyLineLoop(svg, edge, oldEdgeGuides, deltaEdges, numsteps, 25);
+}
+
+function animatePolyLineLoop(svg, edge, lastPoints, deltaEdges, step, time) {
+ for (var i=0; i < deltaEdges.length; ++i) {
+ lastPoints[i][0] += deltaEdges[i][0];
+ lastPoints[i][1] += deltaEdges[i][1];
+ }
+
+ svg.change(edge.line, {points: lastPoints});
+ if (step > 0) {
+ setTimeout(
+ function(){
+ animatePolyLineLoop(svg, edge, lastPoints, deltaEdges, step - 1);
+ },
+ time
+ );
+ }
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/view/admin-setup.js b/azkaban-webserver/src/web/js/azkaban/view/admin-setup.js
new file mode 100644
index 0000000..c3730da
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/admin-setup.js
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var dbUploadPanel;
+azkaban.DBUploadPanel= Backbone.View.extend({
+ events : {
+ "click #upload-jar-btn" : "handleUpload"
+ },
+ initialize : function(settings) {
+ },
+ render: function() {
+ },
+ handleUpload: function(){
+ var filename = $("#file").val();
+ if (filename.length > 4) {
+ var lastIndexOf = filename.lastIndexOf('.');
+ var lastIndexOfForwardSlash = filename.lastIndexOf('\\');
+ var lastIndexOfBackwardSlash = filename.lastIndexOf('/');
+
+ var startIndex = Math.max(lastIndexOfForwardSlash, lastIndexOfBackwardSlash);
+ startIndex += 1;
+
+ var subfilename = filename.substring(startIndex, filename.length);
+ var end = filename.substring(lastIndexOf, filename.length);
+ if (end != ".jar") {
+ alert("File "+ subfilename + " doesn't appear to be a jar. Looking for mysql-connector*.jar");
+ return;
+ }
+ else if (subfilename.substr(0, "mysql-connector".length) != "mysql-connector") {
+ alert("File "+ subfilename + " doesn't appear to be a mysql connector jar. Looking for mysql-connector*.jar");
+ return;
+ }
+
+ console.log("Looks valid, uploading.");
+ var uploadForm = document.getElementById("upload-form");
+ var formData = new FormData(uploadForm);
+ var contextUrl = contextURL;
+
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange=function() {
+ if (xhr.readyState==4) {
+ var data = JSON.parse(xhr.responseText);
+ if (data.error) {
+ alert(data.error);
+ }
+ else {
+ $("#installed").html("Uploaded <span class=bold>" + data.jarname + "</span>");
+ }
+ }
+ }
+ xhr.open("POST", "uploadServlet");
+ xhr.send(formData);
+
+ console.log("Finished.");
+ }
+ else {
+ alert("File doesn't appear to be valid.");
+ }
+ }
+});
+
+var dbConnectionsPanel;
+azkaban.DBConnectionPanel= Backbone.View.extend({
+ events : {
+ "click #save-connection-button" : "handleSaveConnection"
+ },
+ initialize : function(settings) {
+ if (verified) {
+ $("#save-results").text(message);
+ $("#save-results").css("color", "#00CC00");
+ } else {
+ $("#save-results").hide();
+ }
+ },
+ render: function() {
+ },
+ handleSaveConnection: function(){
+ var host = $("#host").val();
+ var port = $("#port").val();
+ var database = $("#database").val();
+ var username = $("#username").val();
+ var password = $("#password").val();
+
+ var contextUrl = contextURL;
+ $.post(
+ contextUrl,
+ {
+ ajax: "saveDbConnection",
+ host: host,
+ port: port,
+ database: database,
+ username: username,
+ password: password
+ },
+ function(data) {
+ if (data.error) {
+ verified = false;
+ $("#save-results").text(data.error);
+ $("#save-results").css("color", "#FF0000");
+ }
+ else if (data.success) {
+ verified = true;
+ $("#save-results").text(data.success);
+ $("#save-results").css("color", "#00CC00");
+ }
+ $("#save-results").show();
+ }
+ );
+ }
+});
+
+$(function() {
+ dbUploadPanel = new azkaban.DBUploadPanel({el:$( '#dbuploadpanel')});
+ dbConnectionPanel = new azkaban.DBConnectionPanel({el:$( '#dbsettingspanel')});
+
+ $("#saveAndContinue").click(function(data) {
+ if (!verified) {
+ alert("The database connection hasn't been verified.");
+ }
+ else {
+ window.location="/?usersetup";
+ }
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/context-menu.js b/azkaban-webserver/src/web/js/azkaban/view/context-menu.js
new file mode 100644
index 0000000..486817e
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/context-menu.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.ContextMenuView = Backbone.View.extend({
+ events: {
+ },
+
+ initialize: function(settings) {
+ var div = this.el;
+ $('body').click(function(e) {
+ $(".contextMenu").remove();
+ });
+ $('body').bind("contextmenu", function(e) {$(".contextMenu").remove()});
+ },
+
+ show: function(evt, menu) {
+ console.log("Show context menu");
+ $(".contextMenu").remove();
+ var x = evt.pageX;
+ var y = evt.pageY;
+
+ var contextMenu = this.setupMenu(menu);
+ $(contextMenu).css({top: y, left: x});
+ $(this.el).after(contextMenu);
+ },
+
+ hide: function(evt) {
+ console.log("Hide context menu");
+ $(".contextMenu").remove();
+ },
+
+ handleClick: function(evt) {
+ console.log("handling click");
+ },
+
+ setupMenu: function(menu) {
+ var contextMenu = document.createElement("div");
+ $(contextMenu).addClass("contextMenu");
+ var ul = document.createElement("ul");
+ $(contextMenu).append(ul);
+
+ for (var i = 0; i < menu.length; ++i) {
+ var menuItem = document.createElement("li");
+ if (menu[i].break) {
+ $(menuItem).addClass("break");
+ $(ul).append(menuItem);
+ continue;
+ }
+ var title = menu[i].title;
+ var callback = menu[i].callback;
+ $(menuItem).addClass("menuitem");
+ $(menuItem).text(title);
+ menuItem.callback = callback;
+ $(menuItem).click(function() {
+ $(contextMenu).hide();
+ this.callback.call();
+ });
+
+ if (menu[i].submenu) {
+ var expandSymbol = document.createElement("div");
+ $(expandSymbol).addClass("expandSymbol");
+ $(menuItem).append(expandSymbol);
+
+ var subMenu = this.setupMenu(menu[i].submenu);
+ $(subMenu).addClass("subMenu");
+ subMenu.parent = contextMenu;
+ menuItem.subMenu = subMenu;
+ $(subMenu).hide();
+ $(this.el).after(subMenu);
+
+ $(menuItem).mouseenter(function() {
+ $(".subMenu").hide();
+ var menuItem = this;
+ menuItem.selected = true;
+ setTimeout(function() {
+ if (menuItem.selected) {
+ var offset = $(menuItem).offset();
+ var left = offset.left;
+ var top = offset.top;
+ var width = $(menuItem).width();
+ var subMenu = menuItem.subMenu;
+
+ var newLeft = left + width - 5;
+ $(subMenu).css({left: newLeft, top: top});
+ $(subMenu).show();
+ }
+ }, 500);
+ });
+ $(menuItem).mouseleave(function() {this.selected = false;});
+ }
+ $(ul).append(menuItem);
+ }
+
+ return contextMenu;
+ }
+});
+
+var contextMenuView;
+$(function() {
+ contextMenuView = new azkaban.ContextMenuView({el:$('#contextMenu')});
+ contextMenuView.hide();
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/executions.js b/azkaban-webserver/src/web/js/azkaban/view/executions.js
new file mode 100644
index 0000000..2724d7c
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/executions.js
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var executionsTabView;
+azkaban.ExecutionsTabView = Backbone.View.extend({
+ events: {
+ 'click #currently-running-view-link': 'handleCurrentlyRunningViewLinkClick',
+ 'click #recently-finished-view-link': 'handleRecentlyFinishedViewLinkClick'
+ },
+
+ initialize: function(settings) {
+ var selectedView = settings.selectedView;
+ if (selectedView == 'recently-finished') {
+ this.handleRecentlyFinishedViewLinkClick();
+ }
+ else {
+ this.handleCurrentlyRunningViewLinkClick();
+ }
+ },
+
+ render: function() {
+ },
+
+ handleCurrentlyRunningViewLinkClick: function() {
+ $('#recently-finished-view-link').removeClass('active');
+ $('#recently-finished-view').hide();
+ $('#currently-running-view-link').addClass('active');
+ $('#currently-running-view').show();
+ },
+
+ handleRecentlyFinishedViewLinkClick: function() {
+ $('#currently-running-view-link').removeClass('active');
+ $('#currently-running-view').hide();
+ $('#recently-finished-view-link').addClass('active');
+ $('#recently-finished-view').show();
+ }
+});
+
+$(function() {
+ executionsTabView = new azkaban.ExecutionsTabView({el: $('#header-tabs')});
+ if (window.location.hash) {
+ var hash = window.location.hash;
+ if (hash == '#recently-finished') {
+ executionsTabView.handleRecentlyFinishedLinkClick();
+ }
+ }
+});
azkaban-webserver/src/web/js/azkaban/view/flow.js 515(+515 -0)
diff --git a/azkaban-webserver/src/web/js/azkaban/view/flow.js b/azkaban-webserver/src/web/js/azkaban/view/flow.js
new file mode 100644
index 0000000..8feca89
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/flow.js
@@ -0,0 +1,515 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var handleJobMenuClick = function(action, el, pos) {
+ var jobid = el[0].jobid;
+ var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" +
+ flowId + "&job=" + jobid;
+ if (action == "open") {
+ window.location.href = requestURL;
+ }
+ else if (action == "openwindow") {
+ window.open(requestURL);
+ }
+}
+
+var flowTabView;
+azkaban.FlowTabView = Backbone.View.extend({
+ events: {
+ "click #graphViewLink": "handleGraphLinkClick",
+ "click #executionsViewLink": "handleExecutionLinkClick",
+ "click #summaryViewLink": "handleSummaryLinkClick"
+ },
+
+ initialize: function(settings) {
+ var selectedView = settings.selectedView;
+ if (selectedView == "executions") {
+ this.handleExecutionLinkClick();
+ }
+ else {
+ this.handleGraphLinkClick();
+ }
+ },
+
+ render: function() {
+ console.log("render graph");
+ },
+
+ handleGraphLinkClick: function(){
+ $("#executionsViewLink").removeClass("active");
+ $("#graphViewLink").addClass("active");
+ $('#summaryViewLink').removeClass('active');
+
+ $("#executionsView").hide();
+ $("#graphView").show();
+ $('#summaryView').hide();
+ },
+
+ handleExecutionLinkClick: function() {
+ $("#graphViewLink").removeClass("active");
+ $("#executionsViewLink").addClass("active");
+ $('#summaryViewLink').removeClass('active');
+
+ $("#graphView").hide();
+ $("#executionsView").show();
+ $('#summaryView').hide();
+ executionModel.trigger("change:view");
+ },
+
+ handleSummaryLinkClick: function() {
+ $('#graphViewLink').removeClass('active');
+ $('#executionsViewLink').removeClass('active');
+ $('#summaryViewLink').addClass('active');
+
+ $('#graphView').hide();
+ $('#executionsView').hide();
+ $('#summaryView').show();
+ },
+});
+
+var jobListView;
+var svgGraphView;
+var executionsView;
+
+azkaban.ExecutionsView = Backbone.View.extend({
+ events: {
+ "click #pageSelection li": "handleChangePageSelection"
+ },
+
+ initialize: function(settings) {
+ this.model.bind('change:view', this.handleChangeView, this);
+ this.model.bind('render', this.render, this);
+ this.model.set({page: 1, pageSize: 16});
+ this.model.bind('change:page', this.handlePageChange, this);
+ },
+
+ render: function(evt) {
+ console.log("render");
+ // Render page selections
+ var tbody = $("#execTableBody");
+ tbody.empty();
+
+ var executions = this.model.get("executions");
+ for (var i = 0; i < executions.length; ++i) {
+ var row = document.createElement("tr");
+
+ var tdId = document.createElement("td");
+ var execA = document.createElement("a");
+ $(execA).attr("href", contextURL + "/executor?execid=" + executions[i].execId);
+ $(execA).text(executions[i].execId);
+ tdId.appendChild(execA);
+ row.appendChild(tdId);
+
+ var tdUser = document.createElement("td");
+ $(tdUser).text(executions[i].submitUser);
+ row.appendChild(tdUser);
+
+ var startTime = "-";
+ if (executions[i].startTime != -1) {
+ var startDateTime = new Date(executions[i].startTime);
+ startTime = getDateFormat(startDateTime);
+ }
+
+ var tdStartTime = document.createElement("td");
+ $(tdStartTime).text(startTime);
+ row.appendChild(tdStartTime);
+
+ var endTime = "-";
+ var lastTime = executions[i].endTime;
+ if (executions[i].endTime != -1) {
+ var endDateTime = new Date(executions[i].endTime);
+ endTime = getDateFormat(endDateTime);
+ }
+ else {
+ lastTime = (new Date()).getTime();
+ }
+
+ var tdEndTime = document.createElement("td");
+ $(tdEndTime).text(endTime);
+ row.appendChild(tdEndTime);
+
+ var tdElapsed = document.createElement("td");
+ $(tdElapsed).text( getDuration(executions[i].startTime, lastTime));
+ row.appendChild(tdElapsed);
+
+ var tdStatus = document.createElement("td");
+ var status = document.createElement("div");
+ $(status).addClass("status");
+ $(status).addClass(executions[i].status);
+ $(status).text(statusStringMap[executions[i].status]);
+ tdStatus.appendChild(status);
+ row.appendChild(tdStatus);
+
+ var tdAction = document.createElement("td");
+ row.appendChild(tdAction);
+
+ tbody.append(row);
+ }
+
+ this.renderPagination(evt);
+ },
+
+ renderPagination: function(evt) {
+ var total = this.model.get("total");
+ total = total? total : 1;
+ var pageSize = this.model.get("pageSize");
+ var numPages = Math.ceil(total / pageSize);
+
+ this.model.set({"numPages": numPages});
+ var page = this.model.get("page");
+
+ //Start it off
+ $("#pageSelection .active").removeClass("active");
+
+ // Disable if less than 5
+ console.log("Num pages " + numPages)
+ var i = 1;
+ for (; i <= numPages && i <= 5; ++i) {
+ $("#page" + i).removeClass("disabled");
+ }
+ for (; i <= 5; ++i) {
+ $("#page" + i).addClass("disabled");
+ }
+
+ // Disable prev/next if necessary.
+ if (page > 1) {
+ $("#previous").removeClass("disabled");
+ $("#previous")[0].page = page - 1;
+ $("#previous a").attr("href", "#page" + (page - 1));
+ }
+ else {
+ $("#previous").addClass("disabled");
+ }
+
+ if (page < numPages) {
+ $("#next")[0].page = page + 1;
+ $("#next").removeClass("disabled");
+ $("#next a").attr("href", "#page" + (page + 1));
+ }
+ else {
+ $("#next")[0].page = page + 1;
+ $("#next").addClass("disabled");
+ }
+
+ // Selection is always in middle unless at barrier.
+ var startPage = 0;
+ var selectionPosition = 0;
+ if (page < 3) {
+ selectionPosition = page;
+ startPage = 1;
+ }
+ else if (page == numPages) {
+ selectionPosition = 5;
+ startPage = numPages - 4;
+ }
+ else if (page == numPages - 1) {
+ selectionPosition = 4;
+ startPage = numPages - 4;
+ }
+ else {
+ selectionPosition = 3;
+ startPage = page - 2;
+ }
+
+ $("#page"+selectionPosition).addClass("active");
+ $("#page"+selectionPosition)[0].page = page;
+ var selecta = $("#page" + selectionPosition + " a");
+ selecta.text(page);
+ selecta.attr("href", "#page" + page);
+
+ for (var j = 0; j < 5; ++j) {
+ var realPage = startPage + j;
+ var elementId = "#page" + (j+1);
+
+ $(elementId)[0].page = realPage;
+ var a = $(elementId + " a");
+ a.text(realPage);
+ a.attr("href", "#page" + realPage);
+ }
+ },
+
+ handleChangePageSelection: function(evt) {
+ if ($(evt.currentTarget).hasClass("disabled")) {
+ return;
+ }
+ var page = evt.currentTarget.page;
+ this.model.set({"page": page});
+ },
+
+ handleChangeView: function(evt) {
+ if (this.init) {
+ return;
+ }
+ console.log("init");
+ this.handlePageChange(evt);
+ this.init = true;
+ },
+
+ handlePageChange: function(evt) {
+ var page = this.model.get("page") - 1;
+ var pageSize = this.model.get("pageSize");
+ var requestURL = contextURL + "/manager";
+
+ var model = this.model;
+ var requestData = {
+ "project": projectName,
+ "flow": flowId,
+ "ajax": "fetchFlowExecutions",
+ "start": page * pageSize,
+ "length": pageSize
+ };
+ var successHandler = function(data) {
+ model.set({
+ "executions": data.executions,
+ "total": data.total
+ });
+ model.trigger("render");
+ };
+ $.get(requestURL, requestData, successHandler, "json");
+ }
+});
+
+var summaryView;
+azkaban.SummaryView = Backbone.View.extend({
+ events: {
+ 'click #analyze-btn': 'fetchLastRun'
+ },
+
+ initialize: function(settings) {
+ this.model.bind('change:view', this.handleChangeView, this);
+ this.model.bind('render', this.render, this);
+
+ this.fetchDetails();
+ this.fetchSchedule();
+ this.model.trigger('render');
+ },
+
+ fetchDetails: function() {
+ var requestURL = contextURL + "/manager";
+ var requestData = {
+ 'ajax': 'fetchflowdetails',
+ 'project': projectName,
+ 'flow': flowId
+ };
+
+ var model = this.model;
+
+ var successHandler = function(data) {
+ console.log(data);
+ model.set({
+ 'jobTypes': data.jobTypes
+ });
+ model.trigger('render');
+ };
+ $.get(requestURL, requestData, successHandler, 'json');
+ },
+
+ fetchSchedule: function() {
+ var requestURL = contextURL + "/schedule"
+ var requestData = {
+ 'ajax': 'fetchSchedule',
+ 'projectId': projectId,
+ 'flowId': flowId
+ };
+ var model = this.model;
+ var view = this;
+ var successHandler = function(data) {
+ model.set({'schedule': data.schedule});
+ model.trigger('render');
+ view.fetchSla();
+ };
+ $.get(requestURL, requestData, successHandler, 'json');
+ },
+
+ fetchSla: function() {
+ var schedule = this.model.get('schedule');
+ if (schedule == null || schedule.scheduleId == null) {
+ return;
+ }
+
+ var requestURL = contextURL + "/schedule"
+ var requestData = {
+ "scheduleId": schedule.scheduleId,
+ "ajax": "slaInfo"
+ };
+ var model = this.model;
+ var successHandler = function(data) {
+ if (data == null || data.settings == null || data.settings.length == 0) {
+ return;
+ }
+ schedule.slaOptions = true;
+ model.set({'schedule': schedule});
+ model.trigger('render');
+ };
+ $.get(requestURL, requestData, successHandler, 'json');
+ },
+
+ fetchLastRun: function() {
+ var requestURL = contextURL + "/manager";
+ var requestData = {
+ 'ajax': 'fetchLastSuccessfulFlowExecution',
+ 'project': projectName,
+ 'flow': flowId
+ };
+ var view = this;
+ var successHandler = function(data) {
+ if (data.success == "false" || data.execId == null) {
+ dust.render("flowstats-no-data", data, function(err, out) {
+ $('#flow-stats-container').html(out);
+ });
+ return;
+ }
+ flowStatsView.show(data.execId);
+ };
+ $.get(requestURL, requestData, successHandler, 'json');
+ },
+
+ handleChangeView: function(evt) {
+ },
+
+ render: function(evt) {
+ var data = {
+ projectName: projectName,
+ flowName: flowId,
+ jobTypes: this.model.get('jobTypes'),
+ schedule: this.model.get('schedule'),
+ };
+ dust.render("flowsummary", data, function(err, out) {
+ $('#summary-view-content').html(out);
+ });
+ },
+});
+
+var graphModel;
+var mainSvgGraphView;
+
+var executionModel;
+azkaban.ExecutionModel = Backbone.Model.extend({});
+
+var summaryModel;
+azkaban.SummaryModel = Backbone.Model.extend({});
+
+var flowStatsView;
+var flowStatsModel;
+
+var executionsTimeGraphView;
+var slaView;
+
+$(function() {
+ var selected;
+ // Execution model has to be created before the window switches the tabs.
+ executionModel = new azkaban.ExecutionModel();
+ executionsView = new azkaban.ExecutionsView({
+ el: $('#executionsView'),
+ model: executionModel
+ });
+
+ summaryModel = new azkaban.SummaryModel();
+ summaryView = new azkaban.SummaryView({
+ el: $('#summaryView'),
+ model: summaryModel
+ });
+
+ flowStatsModel = new azkaban.FlowStatsModel();
+ flowStatsView = new azkaban.FlowStatsView({
+ el: $('#flow-stats-container'),
+ model: flowStatsModel
+ });
+
+ flowTabView = new azkaban.FlowTabView({
+ el: $('#headertabs'),
+ selectedView: selected
+ });
+
+ graphModel = new azkaban.GraphModel();
+ mainSvgGraphView = new azkaban.SvgGraphView({
+ el: $('#svgDiv'),
+ model: graphModel,
+ rightClick: {
+ "node": nodeClickCallback,
+ "edge": edgeClickCallback,
+ "graph": graphClickCallback
+ }
+ });
+
+ jobsListView = new azkaban.JobListView({
+ el: $('#joblist-panel'),
+ model: graphModel,
+ contextMenuCallback: jobClickCallback
+ });
+
+ executionsTimeGraphView = new azkaban.TimeGraphView({
+ el: $('#timeGraph'),
+ model: executionModel,
+ modelField: 'executions'
+ });
+
+ slaView = new azkaban.ChangeSlaView({el:$('#sla-options')});
+
+ var requestURL = contextURL + "/manager";
+ // Set up the Flow options view. Create a new one every time :p
+ $('#executebtn').click(function() {
+ var data = graphModel.get("data");
+ var nodes = data.nodes;
+ var executingData = {
+ project: projectName,
+ ajax: "executeFlow",
+ flow: flowId
+ };
+
+ flowExecuteDialogView.show(executingData);
+ });
+
+ var requestData = {
+ "project": projectName,
+ "ajax": "fetchflowgraph",
+ "flow": flowId
+ };
+ var successHandler = function(data) {
+ console.log("data fetched");
+ graphModel.addFlow(data);
+ graphModel.trigger("change:graph");
+
+ // Handle the hash changes here so the graph finishes rendering first.
+ if (window.location.hash) {
+ var hash = window.location.hash;
+ if (hash == "#executions") {
+ flowTabView.handleExecutionLinkClick();
+ }
+ if (hash == "#summary") {
+ flowTabView.handleSummaryLinkClick();
+ }
+ else if (hash == "#graph") {
+ // Redundant, but we may want to change the default.
+ selected = "graph";
+ }
+ else {
+ if ("#page" == hash.substring(0, "#page".length)) {
+ var page = hash.substring("#page".length, hash.length);
+ console.log("page " + page);
+ flowTabView.handleExecutionLinkClick();
+ executionModel.set({"page": parseInt(page)});
+ }
+ else {
+ selected = "graph";
+ }
+ }
+ }
+ };
+ $.get(requestURL, requestData, successHandler, "json");
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/flow-execution-list.js b/azkaban-webserver/src/web/js/azkaban/view/flow-execution-list.js
new file mode 100644
index 0000000..dff99b4
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/flow-execution-list.js
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ * List of executing jobs on executing flow page.
+ */
+
+var executionListView;
+azkaban.ExecutionListView = Backbone.View.extend({
+ events: {
+ //"contextmenu .flow-progress-bar": "handleProgressBoxClick"
+ },
+
+ initialize: function(settings) {
+ this.model.bind('change:graph', this.renderJobs, this);
+ this.model.bind('change:update', this.updateJobs, this);
+
+ // This is for tabbing. Blah, hacky
+ var executingBody = $("#executableBody")[0];
+ executingBody.level = 0;
+ },
+
+ renderJobs: function(evt) {
+ var data = this.model.get("data");
+ var lastTime = data.endTime == -1 ? (new Date()).getTime() : data.endTime;
+ var executingBody = $("#executableBody");
+ this.updateJobRow(data.nodes, executingBody);
+
+ var flowLastTime = data.endTime == -1 ? (new Date()).getTime() : data.endTime;
+ var flowStartTime = data.startTime;
+ this.updateProgressBar(data, flowStartTime, flowLastTime);
+ },
+
+//
+// handleProgressBoxClick: function(evt) {
+// var target = evt.currentTarget;
+// var job = target.job;
+// var attempt = target.attempt;
+//
+// var data = this.model.get("data");
+// var node = data.nodes[job];
+//
+// var jobId = event.currentTarget.jobid;
+// var requestURL = contextURL + "/manager?project=" + projectName + "&execid=" + execId + "&job=" + job + "&attempt=" + attempt;
+//
+// var menu = [
+// {title: "Open Job...", callback: function() {window.location.href=requestURL;}},
+// {title: "Open Job in New Window...", callback: function() {window.open(requestURL);}}
+// ];
+//
+// contextMenuView.show(evt, menu);
+// },
+
+ updateJobs: function(evt) {
+ var update = this.model.get("update");
+ var lastTime = update.endTime == -1
+ ? (new Date()).getTime()
+ : update.endTime;
+ var executingBody = $("#executableBody");
+
+ if (update.nodes) {
+ this.updateJobRow(update.nodes, executingBody);
+ }
+
+ var data = this.model.get("data");
+ var flowLastTime = data.endTime == -1
+ ? (new Date()).getTime()
+ : data.endTime;
+ var flowStartTime = data.startTime;
+ this.updateProgressBar(data, flowStartTime, flowLastTime);
+ },
+
+ updateJobRow: function(nodes, body) {
+ if (!nodes) {
+ return;
+ }
+
+ nodes.sort(function(a,b) { return a.startTime - b.startTime; });
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i].changedNode ? nodes[i].changedNode : nodes[i];
+
+ if (node.startTime < 0) {
+ continue;
+ }
+ //var nodeId = node.id.replace(".", "\\\\.");
+ var row = node.joblistrow;
+ if (!row) {
+ this.addNodeRow(node, body);
+ }
+
+ row = node.joblistrow;
+ var statusDiv = $(row).find("> td.statustd > .status");
+ statusDiv.text(statusStringMap[node.status]);
+ $(statusDiv).attr("class", "status " + node.status);
+
+ var startTimeTd = $(row).find("> td.startTime");
+ var startdate = new Date(node.startTime);
+ $(startTimeTd).text(getDateFormat(startdate));
+
+ var endTimeTd = $(row).find("> td.endTime");
+ if (node.endTime == -1) {
+ $(endTimeTd).text("-");
+ }
+ else {
+ var enddate = new Date(node.endTime);
+ $(endTimeTd).text(getDateFormat(enddate));
+ }
+
+ var progressBar = $(row).find("> td.timeline > .flow-progress > .main-progress");
+ if (!progressBar.hasClass(node.status)) {
+ for (var j = 0; j < statusList.length; ++j) {
+ var status = statusList[j];
+ progressBar.removeClass(status);
+ }
+ progressBar.addClass(node.status);
+ }
+
+ // Create past attempts
+ if (node.pastAttempts) {
+ for (var a = 0; a < node.pastAttempts.length; ++a) {
+ var attempt = node.pastAttempts[a];
+ var attemptBox = attempt.attemptBox;
+
+ if (!attemptBox) {
+ var attemptBox = document.createElement("div");
+ attempt.attemptBox = attemptBox;
+
+ $(attemptBox).addClass("flow-progress-bar");
+ $(attemptBox).addClass("attempt");
+
+ $(attemptBox).css("float","left");
+ $(attemptBox).bind("contextmenu", attemptRightClick);
+
+ $(progressBar).before(attemptBox);
+ attemptBox.job = node.id;
+ attemptBox.attempt = a;
+ }
+ }
+ }
+
+ var elapsedTime = $(row).find("> td.elapsedTime");
+ if (node.endTime == -1) {
+ $(elapsedTime).text(getDuration(node.startTime, (new Date()).getTime()));
+ }
+ else {
+ $(elapsedTime).text(getDuration(node.startTime, node.endTime));
+ }
+
+ if (node.nodes) {
+ var subtableBody = $(row.subflowrow).find("> td > table");
+ subtableBody[0].level = $(body)[0].level + 1;
+ this.updateJobRow(node.nodes, subtableBody);
+ }
+ }
+ },
+
+ updateProgressBar: function(data, flowStartTime, flowLastTime) {
+ if (data.startTime == -1) {
+ return;
+ }
+
+ var outerWidth = $(".flow-progress").css("width");
+ if (outerWidth) {
+ if (outerWidth.substring(outerWidth.length - 2, outerWidth.length) == "px") {
+ outerWidth = outerWidth.substring(0, outerWidth.length - 2);
+ }
+ outerWidth = parseInt(outerWidth);
+ }
+
+ var parentLastTime = data.endTime == -1 ? (new Date()).getTime() : data.endTime;
+ var parentStartTime = data.startTime;
+
+ var factor = outerWidth / (flowLastTime - flowStartTime);
+ var outerProgressBarWidth = factor * (parentLastTime - parentStartTime);
+ var outerLeftMargin = factor * (parentStartTime - flowStartTime);
+
+ var nodes = data.nodes;
+ for (var i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+
+ // calculate the progress
+ var tr = node.joblistrow;
+ var outerProgressBar = $(tr).find("> td.timeline > .flow-progress");
+ var progressBar = $(tr).find("> td.timeline > .flow-progress > .main-progress");
+ var offsetLeft = 0;
+ var minOffset = 0;
+ progressBar.attempt = 0;
+
+ // Shift the outer progress
+ $(outerProgressBar).css("width", outerProgressBarWidth)
+ $(outerProgressBar).css("margin-left", outerLeftMargin);
+
+ // Add all the attempts
+ if (node.pastAttempts) {
+ var logURL = contextURL + "/executor?execid=" + execId + "&job=" + node.id + "&attempt=" + node.pastAttempts.length;
+ var anchor = $(tr).find("> td.details > a");
+ if (anchor.length != 0) {
+ $(anchor).attr("href", logURL);
+ progressBar.attempt = node.pastAttempts.length;
+ }
+
+ // Calculate the node attempt bars
+ for (var p = 0; p < node.pastAttempts.length; ++p) {
+ var pastAttempt = node.pastAttempts[p];
+ var pastAttemptBox = pastAttempt.attemptBox;
+
+ var left = (pastAttempt.startTime - flowStartTime)*factor;
+ var width = Math.max((pastAttempt.endTime - pastAttempt.startTime)*factor, 3);
+
+ var margin = left - offsetLeft;
+ $(pastAttemptBox).css("margin-left", left - offsetLeft);
+ $(pastAttemptBox).css("width", width);
+
+ $(pastAttemptBox).attr("title", "attempt:" + p + " start:" + getHourMinSec(new Date(pastAttempt.startTime)) + " end:" + getHourMinSec(new Date(pastAttempt.endTime)));
+ offsetLeft += width + margin;
+ }
+ }
+
+ var nodeLastTime = node.endTime == -1 ? (new Date()).getTime() : node.endTime;
+ var left = Math.max((node.startTime-parentStartTime)*factor, minOffset);
+ var margin = left - offsetLeft;
+ var width = Math.max((nodeLastTime - node.startTime)*factor, 3);
+ width = Math.min(width, outerWidth);
+
+ progressBar.css("margin-left", left)
+ progressBar.css("width", width);
+ progressBar.attr("title", "attempt:" + progressBar.attempt + " start:" + getHourMinSec(new Date(node.startTime)) + " end:" + getHourMinSec(new Date(node.endTime)));
+
+ if (node.nodes) {
+ this.updateProgressBar(node, flowStartTime, flowLastTime);
+ }
+ }
+ },
+
+ toggleExpandFlow: function(flow) {
+ console.log("Toggle Expand");
+ var tr = flow.joblistrow;
+ var subFlowRow = tr.subflowrow;
+ var expandIcon = $(tr).find("> td > .listExpand");
+ if (tr.expanded) {
+ tr.expanded = false;
+ $(expandIcon).removeClass("glyphicon-chevron-up");
+ $(expandIcon).addClass("glyphicon-chevron-down");
+
+ $(tr).removeClass("expanded");
+ $(subFlowRow).hide();
+ }
+ else {
+ tr.expanded = true;
+ $(expandIcon).addClass("glyphicon-chevron-up");
+ $(expandIcon).removeClass("glyphicon-chevron-down");
+ $(tr).addClass("expanded");
+ $(subFlowRow).show();
+ }
+ },
+
+ addNodeRow: function(node, body) {
+ var self = this;
+ var tr = document.createElement("tr");
+ var tdName = document.createElement("td");
+ var tdType = document.createElement("td");
+ var tdTimeline = document.createElement("td");
+ var tdStart = document.createElement("td");
+ var tdEnd = document.createElement("td");
+ var tdElapse = document.createElement("td");
+ var tdStatus = document.createElement("td");
+ var tdDetails = document.createElement("td");
+ node.joblistrow = tr;
+ tr.node = node;
+ var padding = 15*$(body)[0].level;
+
+ $(tr).append(tdName);
+ $(tr).append(tdType);
+ $(tr).append(tdTimeline);
+ $(tr).append(tdStart);
+ $(tr).append(tdEnd);
+ $(tr).append(tdElapse);
+ $(tr).append(tdStatus);
+ $(tr).append(tdDetails);
+ $(tr).addClass("jobListRow");
+
+ $(tdName).addClass("jobname");
+ $(tdType).addClass("jobtype");
+ if (padding) {
+ $(tdName).css("padding-left", padding);
+ }
+ $(tdTimeline).addClass("timeline");
+ $(tdStart).addClass("startTime");
+ $(tdEnd).addClass("endTime");
+ $(tdElapse).addClass("elapsedTime");
+ $(tdStatus).addClass("statustd");
+ $(tdDetails).addClass("details");
+
+ $(tdType).text(node.type);
+
+ var outerProgressBar = document.createElement("div");
+ //$(outerProgressBar).attr("id", node.id + "-outerprogressbar");
+ $(outerProgressBar).addClass("flow-progress");
+
+ var progressBox = document.createElement("div");
+ progressBox.job = node.id;
+ //$(progressBox).attr("id", node.id + "-progressbar");
+ $(progressBox).addClass("flow-progress-bar");
+ $(progressBox).addClass("main-progress");
+ $(outerProgressBar).append(progressBox);
+ $(tdTimeline).append(outerProgressBar);
+
+ var requestURL = contextURL + "/manager?project=" + projectName + "&job=" + node.id + "&history";
+ var a = document.createElement("a");
+ $(a).attr("href", requestURL);
+ $(a).text(node.id);
+ $(tdName).append(a);
+ if (node.type=="flow") {
+ var expandIcon = document.createElement("div");
+ $(expandIcon).addClass("listExpand");
+ $(tdName).append(expandIcon);
+ $(expandIcon).addClass("expandarrow glyphicon glyphicon-chevron-down");
+ $(expandIcon).click(function(evt) {
+ var parent = $(evt.currentTarget).parents("tr")[0];
+ self.toggleExpandFlow(parent.node);
+ });
+ }
+
+ var status = document.createElement("div");
+ $(status).addClass("status");
+ //$(status).attr("id", node.id + "-status-div");
+ tdStatus.appendChild(status);
+
+ var logURL = contextURL + "/executor?execid=" + execId + "&job=" + node.nestedId;
+ if (node.attempt) {
+ logURL += "&attempt=" + node.attempt;
+ }
+
+ if (node.type != 'flow' && node.status != 'SKIPPED') {
+ var a = document.createElement("a");
+ $(a).attr("href", logURL);
+ //$(a).attr("id", node.id + "-log-link");
+ $(a).text("Details");
+ $(tdDetails).append(a);
+ }
+
+ $(body).append(tr);
+ if (node.type == "flow") {
+ var subFlowRow = document.createElement("tr");
+ var subFlowCell = document.createElement("td");
+ $(subFlowCell).addClass("subflowrow");
+
+ var numColumn = $(tr).children("td").length;
+ $(subFlowCell).attr("colspan", numColumn);
+ tr.subflowrow = subFlowRow;
+
+ $(subFlowRow).append(subFlowCell);
+ $(body).append(subFlowRow);
+ $(subFlowRow).hide();
+ var subtable = document.createElement("table");
+ var parentClasses = $(body).closest("table").attr("class");
+
+ $(subtable).attr("class", parentClasses);
+ $(subtable).addClass("subtable");
+ $(subFlowCell).append(subtable);
+ }
+ }
+});
+
+var attemptRightClick = function(event) {
+ var target = event.currentTarget;
+ var job = target.job;
+ var attempt = target.attempt;
+
+ var jobId = event.currentTarget.jobid;
+ var requestURL = contextURL + "/executor?project=" + projectName + "&execid=" + execId + "&job=" + job + "&attempt=" + attempt;
+
+ var menu = [
+ {title: "Open Attempt Log...", callback: function() {window.location.href=requestURL;}},
+ {title: "Open Attempt Log in New Window...", callback: function() {window.open(requestURL);}}
+ ];
+
+ contextMenuView.show(event, menu);
+ return false;
+}
+
diff --git a/azkaban-webserver/src/web/js/azkaban/view/flow-extended.js b/azkaban-webserver/src/web/js/azkaban/view/flow-extended.js
new file mode 100644
index 0000000..686d0a1
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/flow-extended.js
@@ -0,0 +1,62 @@
+azkaban.FlowExtendedViewPanel = Backbone.View.extend({
+ events: {
+ "click .closeInfoPanel" : "handleClosePanel"
+ },
+ initialize: function(settings) {
+ //this.model.bind('change:flowinfo', this.changeFlowInfo, this);
+ $(this.el).show();
+ $(this.el).draggable({cancel: ".dataContent", containment: "document"});
+
+ this.render();
+ $(this.el).hide();
+ },
+ showExtendedView: function(evt) {
+ var event = evt;
+
+ $(this.el).css({top: evt.pageY, left: evt.pageX});
+ $(this.el).show();
+ },
+ render: function(self) {
+ console.log("Changing title");
+ $(this.el).find(".nodeId").text(this.model.get("id"));
+ $(this.el).find(".nodeType").text(this.model.get("type"));
+
+ var props = this.model.get("props");
+ var tableBody = $(this.el).find(".dataPropertiesBody");
+
+ for (var key in props) {
+ var tr = document.createElement("tr");
+ var tdKey = document.createElement("td");
+ var tdValue = document.createElement("td");
+
+ $(tdKey).text(key);
+ $(tdValue).text(props[key]);
+
+ $(tr).append(tdKey);
+ $(tr).append(tdValue);
+
+ $(tableBody).append(tr);
+
+ var propsTable = $(this.el).find(".dataJobProperties");
+ $(propsTable).resizable({handler: "s"});
+ }
+
+ if (this.model.get("type") == "flow") {
+ var svgns = "http://www.w3.org/2000/svg";
+ var svgDataFlow = $(this.el).find(".dataFlow");
+
+ var svgGraph = document.createElementNS(svgns, "svg");
+ $(svgGraph).attr("class", "svgTiny");
+ $(svgDataFlow).append(svgGraph);
+ $(svgDataFlow).resizable();
+
+ this.graphView = new azkaban.SvgGraphView({el: svgDataFlow, model: this.model, render: true, rightClick: { "node": nodeClickCallback, "graph": graphClickCallback }})
+ }
+ else {
+ $(this.el).find(".dataFlow").hide();
+ }
+ },
+ handleClosePanel: function(self) {
+ $(this.el).hide();
+ }
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/flow-stats.js b/azkaban-webserver/src/web/js/azkaban/view/flow-stats.js
new file mode 100644
index 0000000..a77c3f8
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/flow-stats.js
@@ -0,0 +1,359 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.FlowStatsModel = Backbone.Model.extend({});
+azkaban.FlowStatsView = Backbone.View.extend({
+ events: {
+ },
+
+ histogram: true,
+
+ initialize: function(settings) {
+ this.model.bind('change:view', this.handleChangeView, this);
+ this.model.bind('render', this.render, this);
+ if (settings.histogram != null) {
+ this.histogram = settings.histogram;
+ }
+ },
+
+ render: function(evt) {
+ },
+
+ show: function(execId) {
+ this.analyzeExecution(execId);
+ },
+
+ fetchJobs: function(execId) {
+ var requestURL = contextURL + "/executor";
+ var requestData = {"execid": execId, "ajax":"fetchexecflow"};
+ var jobs = [];
+ var successHandler = function(data) {
+ data.nodes.sort(function(a, b) {
+ return a.startTime - b.startTime;
+ });
+ jobs = data.nodes;
+ };
+ $.ajax({
+ url: requestURL,
+ data: requestData,
+ success: successHandler,
+ dataType: "json",
+ async: false
+ });
+ return jobs;
+ },
+
+ fetchJobStats: function(jobId, execId) {
+ var requestURL = contextURL + "/executor";
+ var requestData = {
+ "execid": execId,
+ "flowid": flowId,
+ "jobid": jobId,
+ "ajax": "fetchExecJobStats"
+ };
+ var stats = null;
+ var successHandler = function(data) {
+ stats = data;
+ };
+ $.ajax({
+ url: requestURL,
+ data: requestData,
+ success: successHandler,
+ dataType: "json",
+ async: false
+ });
+ return stats;
+ },
+
+ updateStatsMapred: function(state, data, job) {
+ var stats = data.stats;
+ var mappers = parseInt(state.totalMappers);
+ var reducers = parseInt(state.totalReducers);
+ if (mappers >= stats.mapSlots.max) {
+ stats.mapSlots.max = mappers;
+ stats.mapSlots.job = job;
+ }
+ if (reducers >= stats.reduceSlots.max) {
+ stats.reduceSlots.max = reducers;
+ stats.reduceSlots.job = job;
+ }
+ stats.totalMapSlots += mappers;
+ stats.totalReduceSlots += reducers;
+
+ },
+
+ updateStatsConf: function(conf, data, job) {
+ var stats = data.stats;
+ if (conf == null) {
+ data.warnings.push("No job conf available for job " + job);
+ return;
+ }
+
+ var jobMapMemoryMb = parseInt(conf['mapred.job.map.memory.mb']);
+ if (jobMapMemoryMb >= stats.jobMapMemoryMb.max) {
+ stats.jobMapMemoryMb.max = jobMapMemoryMb;
+ stats.jobMapMemoryMb.job = job;
+ }
+ var jobReduceMemoryMb = parseInt(conf['mapred.job.reduce.memory.mb']);
+ if (jobReduceMemoryMb >= stats.jobReduceMemoryMb.max) {
+ stats.jobReduceMemoryMb.max = jobReduceMemoryMb;
+ stats.jobReduceMemoryMb.job = job;
+ }
+
+ var childJavaOpts = conf['mapred.child.java.opts'];
+ var parts = childJavaOpts.split(" ");
+ for (var i = 0; i < parts.length; ++i) {
+ var str = parts[i];
+ if (str.indexOf('Xmx') > -1) {
+ if (str.length <= 4) {
+ continue;
+ }
+ var size = str.substring(4, str.length);
+ var val = sizeStrToBytes(size);
+ if (val >= stats.xmx.max) {
+ stats.xmx.max = val;
+ stats.xmx.str = size;
+ stats.xmx.job = job;
+ }
+ }
+ if (str.indexOf('Xms') > -1) {
+ if (str.length <= 4) {
+ continue;
+ }
+ var size = str.substring(4, str.length);
+ var val = sizeStrToBytes(size);
+ stats.xms.set = true;
+ if (val >= stats.xms.max) {
+ stats.xms.max = val;
+ stats.xms.str = size;
+ stats.xms.job = job;
+ }
+ }
+ }
+
+ var cacheFiles = conf['mapred.cache.files'];
+ var cacheFilesFilesizes = conf['mapred.cache.files.filesizes'];
+ if (cacheFiles != null && cacheFilesFilesizes != null) {
+ stats.distributedCache.using = true;
+ var parts = cacheFilesFilesizes.split(',');
+ var size = 0;
+ for (var i = 0; i < parts.length; ++i) {
+ size += parseInt(parts[i]);
+ }
+ if (size >= stats.distributedCache.max) {
+ stats.distributedCache.max = size;
+ stats.distributedCache.job = job;
+ }
+ }
+ },
+
+ updateStatsCounters: function(state, data, job) {
+ var stats = data.stats;
+ if (state.counters == null) {
+ data.warnings.push("No job counters available for job " + job);
+ return;
+ }
+ var fileSystemCounters = state.counters['FileSystemCounters'];
+ if (fileSystemCounters == null) {
+ data.warnings.push("No FileSystemCounters available for job " + job);
+ return;
+ }
+ var fileBytesRead = parseInt(fileSystemCounters['FILE_BYTES_READ']);
+ if (fileBytesRead >= stats.fileBytesRead.max) {
+ stats.fileBytesRead.max = fileBytesRead;
+ stats.fileBytesRead.job = job;
+ }
+
+ var fileBytesWritten = parseInt(fileSystemCounters['FILE_BYTES_WRITTEN']);
+ if (fileBytesWritten >= stats.fileBytesWritten.max) {
+ stats.fileBytesWritten.max = fileBytesWritten;
+ stats.fileBytesWritten.job = job;
+ }
+
+ var hdfsBytesRead = parseInt(fileSystemCounters['HDFS_BYTES_READ']);
+ if (hdfsBytesRead >= stats.hdfsBytesRead.max) {
+ stats.hdfsBytesRead.max = hdfsBytesRead;
+ stats.hdfsBytesRead.job = job;
+ }
+
+ var hdfsBytesWritten = parseInt(fileSystemCounters['HDFS_BYTES_WRITTEN']);
+ if (hdfsBytesWritten >= stats.hdfsBytesWritten.max) {
+ stats.hdfsBytesWritten.max = hdfsBytesWritten;
+ stats.hdfsBytesWritten.job = job;
+ }
+ },
+
+ updateStats: function(jobStats, data, job) {
+ var stats = data.stats;
+ var state = jobStats.state;
+ var conf = jobStats.conf;
+
+ this.updateStatsMapred(state, data, job);
+ this.updateStatsConf(conf, data, job);
+ this.updateStatsCounters(state, data, job);
+ },
+
+ finalizeStats: function(data) {
+ data.success = true;
+ },
+
+ analyzeExecution: function(execId) {
+ var jobs = this.fetchJobs(execId);
+ if (jobs == null) {
+ this.model.set({'data': null});
+ this.model.trigger('render');
+ return;
+ }
+
+ var data = {
+ success: false,
+ message: null,
+ warnings: [],
+ durations: [],
+ histogram: this.histogram,
+ stats: {
+ mapSlots: {
+ max: 0,
+ job: null
+ },
+ reduceSlots: {
+ max: 0,
+ job: null
+ },
+ totalMapSlots: 0,
+ totalReduceSlots: 0,
+ numJobs: jobs.length,
+ longestTaskTime: 0,
+ jobMapMemoryMb: {
+ max: 0,
+ job: null
+ },
+ jobReduceMemoryMb: {
+ max: 0,
+ job: null
+ },
+ xmx: {
+ max: 0,
+ str: null,
+ job: null
+ },
+ xms: {
+ set: false,
+ max: 0,
+ str: null,
+ job: null
+ },
+ fileBytesRead: {
+ max: 0,
+ job: null
+ },
+ hdfsBytesRead: {
+ max: 0,
+ job: null
+ },
+ fileBytesWritten: {
+ max: 0,
+ job: null
+ },
+ hdfsBytesWritten: {
+ max: 0,
+ job: null
+ },
+ distributedCache: {
+ using: false,
+ max: 0,
+ job: null
+ },
+ }
+ };
+
+ var jobsAnalyzed = 0;
+ for (var i = 0; i < jobs.length; ++i) {
+ var job = jobs[i];
+ var duration = job.endTime - job.startTime;
+ data.durations.push({
+ job: job.id,
+ duration: duration
+ });
+
+ var jobStats = this.fetchJobStats(job.id, execId);
+ if (jobStats.jobStats == null) {
+ data.warnings.push("No job stats available for job " + job.id);
+ continue;
+ }
+ for (var j = 0; j < jobStats.jobStats.length; ++j) {
+ this.updateStats(jobStats.jobStats[j], data, job.id);
+ }
+ ++jobsAnalyzed;
+ }
+
+ // If no jobs were analyzed, then no jobs had any job stats available. In
+ // this case, display a No Flow Stats Available message.
+ if (jobsAnalyzed == 0) {
+ data.success = false;
+ data.message = "There were no job stats provided by any job.";
+ }
+ else {
+ this.finalizeStats(data);
+ }
+
+ this.model.set({'data': data});
+ this.model.trigger('render');
+ },
+
+ render: function(evt) {
+ var view = this;
+ var data = this.model.get('data');
+ if (data == null) {
+ var msg = { message: "Error retrieving flow stats."};
+ dust.render("flowstats-no-data", msg, function(err, out) {
+ view.display(out);
+ });
+ }
+ else if (data.success == false) {
+ dust.render("flowstats-no-data", data, function(err, out) {
+ view.display(out);
+ });
+ }
+ else {
+ var histogram = this.histogram;
+ dust.render("flowstats", data, function(err, out) {
+ view.display(out);
+ if (histogram == true) {
+ var yLabelFormatCallback = function(y) {
+ var seconds = y / 1000.0;
+ return seconds.toString() + " s";
+ };
+
+ Morris.Bar({
+ element: "job-histogram",
+ data: data.durations,
+ xkey: "job",
+ ykeys: ["duration"],
+ labels: ["Duration"],
+ yLabelFormat: yLabelFormatCallback
+ });
+ }
+ });
+ }
+ },
+
+ display: function(out) {
+ $('#flow-stats-container').html(out);
+ },
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/history.js b/azkaban-webserver/src/web/js/azkaban/view/history.js
new file mode 100644
index 0000000..11401b1
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/history.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var advFilterView;
+azkaban.AdvFilterView = Backbone.View.extend({
+ events: {
+ "click #filter-btn": "handleAdvFilter"
+ },
+
+ initialize: function(settings) {
+ $('#datetimebegin').datetimepicker();
+ $('#datetimeend').datetimepicker();
+ $('#datetimebegin').on('change.dp', function(e) {
+ $('#datetimeend').data('DateTimePicker').setStartDate(e.date);
+ });
+ $('#datetimeend').on('change.dp', function(e) {
+ $('#datetimebegin').data('DateTimePicker').setEndDate(e.date);
+ });
+ $('#adv-filter-error-msg').hide();
+ },
+
+ handleAdvFilter: function(evt) {
+ console.log("handleAdv");
+ var projcontain = $('#projcontain').val();
+ var flowcontain = $('#flowcontain').val();
+ var usercontain = $('#usercontain').val();
+ var status = $('#status').val();
+ var begin = $('#datetimebegin').val();
+ var end = $('#datetimeend').val();
+
+ console.log("filtering history");
+
+ var historyURL = contextURL + "/history"
+ var redirectURL = contextURL + "/schedule"
+
+ var requestURL = historyURL + "?advfilter=true" + "&projcontain=" + projcontain + "&flowcontain=" + flowcontain + "&usercontain=" + usercontain + "&status=" + status + "&begin=" + begin + "&end=" + end ;
+ window.location = requestURL;
+
+ /*
+ var requestData = {
+ "action": "advfilter",
+ "projre": projre,
+ "flowre": flowre,
+ "userre": userre
+ };
+ var successHandler = function(data) {
+ if (data.action == "redirect") {
+ window.location = data.redirect;
+ }
+ };
+ $.get(historyURL, requestData, successHandler, "json");
+ */
+ },
+
+ render: function() {
+ }
+});
+
+$(function() {
+ filterView = new azkaban.AdvFilterView({el: $('#adv-filter')});
+ $('#adv-filter-btn').click( function() {
+ $('#adv-filter').modal();
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/history-day.js b/azkaban-webserver/src/web/js/azkaban/view/history-day.js
new file mode 100644
index 0000000..732028b
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/history-day.js
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var dayDataModel;
+azkaban.DayDataModel = Backbone.Model.extend({});
+
+var dayByDayView;
+azkaban.DayByDayView = Backbone.View.extend({
+ events: {
+ },
+ initialize: function(settings) {
+ this.svgns = "http://www.w3.org/2000/svg";
+ this.svg = $(this.el).find('svg')[0];
+ this.columnDayWidth = 100;
+ this.columnHourHeight = 50;
+ this.columnHeight = 50*24;
+
+ this.render(this);
+ },
+ prepareData: function(self) {
+ var response = model.get("data");
+ var start = data.start;
+ var end = data.end;
+ var data = data.data;
+
+ var daysData = {};
+
+ var startDate = new Date(start);
+
+ while (startDate.getTime() < end) {
+ daysData[startDate.getTime()] = new Array();
+ startDate.setDate(startDate.getDate() + 1);
+ }
+
+ for (var i = 0; i < data.length; ++i) {
+ var flow = data[i];
+
+ }
+ },
+ render: function(self) {
+ var svg = self.svg;
+ var svgns = self.svgns;
+ var width = $(svg).width();
+ var height = $(svg).height();
+
+ var mainG = document.createElementNS(this.svgns, 'g');
+ $(svg).append(mainG);
+ }
+});
+
+var showDialog = function(title, message) {
+ $('#messageTitle').text(title);
+ $('#messageBox').text(message);
+
+ $('#messageDialog').modal({
+ closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+ position: ["20%",],
+ containerId: 'confirm-container',
+ containerCss: {
+ 'height': '220px',
+ 'width': '565px'
+ },
+ onShow: function (dialog) {
+ }
+ });
+}
+
+
+$(function() {
+ var requestURL = contextURL + "/history";
+
+ var start = new Date();
+ start.setHours(0);
+ start.setMinutes(0);
+ start.setSeconds(0);
+ start.setMilliseconds(0);
+ var end = new Date(start);
+
+ start.setDate(start.getDate() - 7);
+ console.log(start.getTime());
+
+ end.setDate(end.getDate() + 1);
+ console.log(end.getTime());
+
+ dayDataModel = new azkaban.DayDataModel();
+ dayByDayView = new azkaban.DayByDayView({el:$('#dayByDayPanel'), model: dayDataModel});
+
+ $.get(
+ requestURL,
+ {"ajax":"fetch", "start": start.getTime(), "end": end.getTime()},
+ function(data) {
+ dayDataModel.set({data:data});
+ },
+ "json");
+});
azkaban-webserver/src/web/js/azkaban/view/jmx.js 152(+152 -0)
diff --git a/azkaban-webserver/src/web/js/azkaban/view/jmx.js b/azkaban-webserver/src/web/js/azkaban/view/jmx.js
new file mode 100644
index 0000000..8f12f1e
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/jmx.js
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var jmxTableView;
+azkaban.JMXTableView = Backbone.View.extend({
+ events: {
+ "click .query-btn": "queryJMX",
+ "click .collapse-btn": "collapseRow"
+ },
+
+ initialize: function(settings) {
+ },
+
+ formatValue: function(value) {
+ if (String(value).length != TIMESTAMP_LENGTH) {
+ return value;
+ }
+ if (isNaN(parseInt(value))) {
+ return value;
+ }
+ var date = new Date(value);
+ if (date.getTime() <= 0) {
+ return value;
+ }
+ return value + " (" + date.toISOString() + ")";
+ },
+
+ queryJMX: function(evt) {
+ var target = evt.currentTarget;
+ var id = target.id;
+
+ var childID = id + "-child";
+ var tbody = id + "-tbody";
+
+ var requestURL = contextURL + "/jmx";
+ var canonicalName=$(target).attr("domain") + ":name=" + $(target).attr("name");
+
+ var data = {
+ "ajax": "getAllMBeanAttributes",
+ "mBean": canonicalName
+ };
+ if ($(target).attr("hostPort")) {
+ data.ajax = "getAllExecutorAttributes";
+ data.hostPort = $(target).attr("hostPort");
+ }
+ var view = this;
+ var successHandler = function(data) {
+ var table = $('#' + tbody);
+ $(table).empty();
+
+ for (var key in data.attributes) {
+ var value = data.attributes[key];
+
+ var tr = document.createElement("tr");
+ var tdName = document.createElement("td");
+ var tdVal = document.createElement("td");
+
+ $(tdName).addClass('property-key');
+ $(tdName).text(key);
+
+ value = view.formatValue(value);
+ $(tdVal).text(value);
+
+ $(tr).append(tdName);
+ $(tr).append(tdVal);
+
+ $('#' + tbody).append(tr);
+ }
+
+ var child = $("#" + childID);
+ $(child).fadeIn();
+ };
+ $.get(requestURL, data, successHandler);
+ },
+
+ queryRemote: function(evt) {
+ var target = evt.currentTarget;
+ var id = target.id;
+
+ var childID = id + "-child";
+ var tbody = id + "-tbody";
+
+ var requestURL = contextURL + "/jmx";
+ var canonicalName = $(target).attr("domain") + ":name=" + $(target).attr("name");
+ var hostPort = $(target).attr("hostport");
+ var requestData = {
+ "ajax": "getAllExecutorAttributes",
+ "mBean": canonicalName,
+ "hostPort": hostPort
+ };
+ var view = this;
+ var successHandler = function(data) {
+ var table = $('#' + tbody);
+ $(table).empty();
+
+ for (var key in data.attributes) {
+ var value = data.attributes[key];
+
+ var tr = document.createElement("tr");
+ var tdName = document.createElement("td");
+ var tdVal = document.createElement("td");
+
+ $(tdName).addClass('property-key');
+ $(tdName).text(key);
+
+ value = view.formatValue(value);
+ $(tdVal).text(value);
+
+ $(tr).append(tdName);
+ $(tr).append(tdVal);
+
+ $('#' + tbody).append(tr);
+ }
+
+ var child = $("#" + childID);
+ $(child).fadeIn();
+ };
+ $.get(requestURL, requestData, successHandler);
+ },
+
+ collapseRow: function(evt) {
+ $(evt.currentTarget).parent().parent().fadeOut();
+ },
+
+ render: function() {
+ }
+});
+
+var remoteTables = new Array();
+$(function() {
+ jmxTableView = new azkaban.JMXTableView({el:$('#all-jmx')});
+
+ $(".remoteJMX").each(function(item) {
+ var newTableView = new azkaban.JMXTableView({el:$(this)});
+ remoteTables.push(newTableView);
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/job-details.js b/azkaban-webserver/src/web/js/azkaban/view/job-details.js
new file mode 100644
index 0000000..8063797
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/job-details.js
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var jobLogView;
+azkaban.JobLogView = Backbone.View.extend({
+ events: {
+ "click #updateLogBtn" : "refresh"
+ },
+
+ initialize: function() {
+ this.listenTo(this.model, "change:logData", this.render);
+ },
+
+ refresh: function() {
+ this.model.refresh();
+ },
+
+ render: function() {
+ var re = /(https?:\/\/(([-\w\.]+)+(:\d+)?(\/([\w/_\.]*(\?\S+)?)?)?))/g;
+ var log = this.model.get("logData");
+ log = log.replace(re, "<a href=\"$1\" title=\"\">$1</a>");
+ $("#logSection").html(log);
+ }
+});
+
+var showDialog = function(title, message) {
+ $('#messageTitle').text(title);
+ $('#messageBox').text(message);
+ $('#messageDialog').modal({
+ closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+ position: ["20%",],
+ containerId: 'confirm-container',
+ containerCss: {
+ 'height': '220px',
+ 'width': '565px'
+ },
+ onShow: function (dialog) {
+ }
+ });
+}
+
+$(function() {
+ var jobLogModel = new azkaban.JobLogModel();
+ jobLogView = new azkaban.JobLogView({
+ el: $('#jobLogView'),
+ model: jobLogModel
+ });
+ jobLogModel.refresh();
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/job-edit.js b/azkaban-webserver/src/web/js/azkaban/view/job-edit.js
new file mode 100644
index 0000000..073515e
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/job-edit.js
@@ -0,0 +1,260 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var jobEditView;
+azkaban.JobEditView = Backbone.View.extend({
+ events : {
+ "click" : "closeEditingTarget",
+ "click #set-btn": "handleSet",
+ "click #cancel-btn": "handleCancel",
+ "click #close-btn": "handleCancel",
+ "click #add-btn": "handleAddRow",
+ "click table .editable": "handleEditColumn",
+ "click table .remove-btn": "handleRemoveColumn"
+ },
+
+ initialize: function(setting) {
+ this.projectURL = contextURL + "manager"
+ this.generalParams = {}
+ this.overrideParams = {}
+ },
+
+ handleCancel: function(evt) {
+ $('#job-edit-pane').hide();
+ var tbl = document.getElementById("generalProps").tBodies[0];
+ var rows = tbl.rows;
+ var len = rows.length;
+ for (var i = 0; i < len-1; i++) {
+ tbl.deleteRow(0);
+ }
+ },
+
+ show: function(projectName, flowName, jobName) {
+ this.projectName = projectName;
+ this.flowName = flowName;
+ this.jobName = jobName;
+
+ var projectURL = this.projectURL
+
+ $('#job-edit-pane').modal();
+
+ var handleAddRow = this.handleAddRow;
+
+ /*var overrideParams;
+ var generalParams;
+ this.overrideParams = overrideParams;
+ this.generalParams = generalParams;*/
+ var fetchJobInfo = {
+ "project": this.projectName,
+ "ajax": "fetchJobInfo",
+ "flowName": this.flowName,
+ "jobName": this.jobName
+ };
+ var mythis = this;
+ var fetchJobSuccessHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ return;
+ }
+ document.getElementById('jobName').innerHTML = data.jobName;
+ document.getElementById('jobType').innerHTML = data.jobType;
+ var generalParams = data.generalParams;
+ var overrideParams = data.overrideParams;
+
+ /*for (var key in generalParams) {
+ var row = handleAddRow();
+ var td = $(row).find('span');
+ $(td[1]).text(key);
+ $(td[2]).text(generalParams[key]);
+ }*/
+
+ mythis.overrideParams = overrideParams;
+ mythis.generalParams = generalParams;
+
+ for (var okey in overrideParams) {
+ if (okey != 'type' && okey != 'dependencies') {
+ var row = handleAddRow();
+ var td = $(row).find('span');
+ $(td[0]).text(okey);
+ $(td[1]).text(overrideParams[okey]);
+ }
+ }
+ };
+
+ $.get(projectURL, fetchJobInfo, fetchJobSuccessHandler, "json");
+ },
+
+ handleSet: function(evt) {
+ this.closeEditingTarget(evt);
+ var jobOverride = {};
+ var editRows = $(".editRow");
+ for (var i = 0; i < editRows.length; ++i) {
+ var row = editRows[i];
+ var td = $(row).find('span');
+ var key = $(td[0]).text();
+ var val = $(td[1]).text();
+
+ if (key && key.length > 0) {
+ jobOverride[key] = val;
+ }
+ }
+
+ var overrideParams = this.overrideParams
+ var generalParams = this.generalParams
+
+ jobOverride['type'] = overrideParams['type']
+ if ('dependencies' in overrideParams) {
+ jobOverride['dependencies'] = overrideParams['dependencies']
+ }
+
+ var project = this.projectName
+ var flowName = this.flowName
+ var jobName = this.jobName
+
+ var jobOverrideData = {
+ project: project,
+ flowName: flowName,
+ jobName: jobName,
+ ajax: "setJobOverrideProperty",
+ jobOverride: jobOverride
+ };
+
+ var projectURL = this.projectURL
+ var redirectURL = projectURL+'?project='+project+'&flow='+flowName+'&job='+jobName;
+ var jobOverrideSuccessHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ }
+ else {
+ window.location = redirectURL;
+ }
+ };
+
+ $.get(projectURL, jobOverrideData, jobOverrideSuccessHandler, "json");
+ },
+
+ handleAddRow: function(evt) {
+ var tr = document.createElement("tr");
+ var tdName = document.createElement("td");
+ $(tdName).addClass('property-key');
+ var tdValue = document.createElement("td");
+
+ var remove = document.createElement("div");
+ $(remove).addClass("pull-right").addClass('remove-btn');
+ var removeBtn = document.createElement("button");
+ $(removeBtn).attr('type', 'button');
+ $(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
+ $(removeBtn).text('Delete');
+ $(remove).append(removeBtn);
+
+ var nameData = document.createElement("span");
+ $(nameData).addClass("spanValue");
+ var valueData = document.createElement("span");
+ $(valueData).addClass("spanValue");
+
+ $(tdName).append(nameData);
+ $(tdName).addClass("editable");
+ nameData.myparent = tdName;
+
+ $(tdValue).append(valueData);
+ $(tdValue).append(remove);
+ $(tdValue).addClass("editable");
+ $(tdValue).addClass("value");
+ valueData.myparent = tdValue;
+
+ $(tr).addClass("editRow");
+ $(tr).append(tdName);
+ $(tr).append(tdValue);
+
+ $(tr).insertBefore("#addRow");
+ return tr;
+ },
+
+ handleEditColumn: function(evt) {
+ var curTarget = evt.currentTarget;
+ if (this.editingTarget != curTarget) {
+ this.closeEditingTarget(evt);
+
+ var text = $(curTarget).children(".spanValue").text();
+ $(curTarget).empty();
+
+ var input = document.createElement("input");
+ $(input).attr("type", "text");
+ $(input).addClass("form-control").addClass("input-sm");
+ $(input).val(text);
+
+ $(curTarget).addClass("editing");
+ $(curTarget).append(input);
+ $(input).focus();
+ var obj = this;
+ $(input).keypress(function(evt) {
+ if (evt.which == 13) {
+ obj.closeEditingTarget(evt);
+ }
+ });
+ this.editingTarget = curTarget;
+ }
+
+ evt.preventDefault();
+ evt.stopPropagation();
+ },
+
+ handleRemoveColumn: function(evt) {
+ var curTarget = evt.currentTarget;
+ // Should be the table
+ var row = curTarget.parentElement.parentElement;
+ $(row).remove();
+ },
+
+ closeEditingTarget: function(evt) {
+ if (this.editingTarget == null ||
+ this.editingTarget == evt.target ||
+ this.editingTarget == evt.target.myparent) {
+ return;
+ }
+ var input = $(this.editingTarget).children("input")[0];
+ var text = $(input).val();
+ $(input).remove();
+
+ var valueData = document.createElement("span");
+ $(valueData).addClass("spanValue");
+ $(valueData).text(text);
+
+ if ($(this.editingTarget).hasClass("value")) {
+ var remove = document.createElement("div");
+ $(remove).addClass("pull-right").addClass('remove-btn');
+ var removeBtn = document.createElement("button");
+ $(removeBtn).attr('type', 'button');
+ $(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
+ $(removeBtn).text('Delete');
+ $(remove).append(removeBtn);
+ $(this.editingTarget).append(remove);
+ }
+
+ $(this.editingTarget).removeClass("editing");
+ $(this.editingTarget).append(valueData);
+ valueData.myparent = this.editingTarget;
+ this.editingTarget = null;
+ }
+});
+
+$(function() {
+ jobEditView = new azkaban.JobEditView({
+ el: $('#job-edit-pane')
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/job-history.js b/azkaban-webserver/src/web/js/azkaban/view/job-history.js
new file mode 100644
index 0000000..f2119f5
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/job-history.js
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+
+$.namespace('azkaban');
+
+var jobHistoryView;
+
+var dataModel;
+azkaban.DataModel = Backbone.Model.extend({});
+
+$(function() {
+ var selected;
+ var series = dataSeries;
+ dataModel = new azkaban.DataModel();
+ dataModel.set({
+ "data": series
+ });
+ dataModel.trigger('render');
+
+ jobHistoryView = new azkaban.TimeGraphView({
+ el: $('#timeGraph'),
+ model: dataModel,
+ modelField: "data"
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/job-list.js b/azkaban-webserver/src/web/js/azkaban/view/job-list.js
new file mode 100644
index 0000000..b9b7f8b
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/job-list.js
@@ -0,0 +1,344 @@
+/*
+ * 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.
+ */
+
+/*
+ * Job list sidebar view to accompany SVG graph.
+ */
+
+azkaban.JobListView = Backbone.View.extend({
+ events: {
+ "keyup input": "filterJobs",
+ "click li.listElement": "handleJobClick",
+ "click #resetPanZoomBtn": "handleResetPanZoom",
+ "click #autoPanZoomBtn": "handleAutoPanZoom",
+ "contextmenu li.listElement": "handleContextMenuClick",
+ "click .expandarrow": "handleToggleMenuExpand",
+ "click #close-btn" : "handleClose"
+ },
+
+ initialize: function(settings) {
+ this.model.bind('change:selected', this.handleSelectionChange, this);
+ this.model.bind('change:disabled', this.handleDisabledChange, this);
+ this.model.bind('change:graph', this.render, this);
+ this.model.bind('change:update', this.handleStatusUpdate, this);
+
+ $("#open-joblist-btn").click(this.handleOpen);
+ $("#joblist-panel").hide();
+
+ this.filterInput = $(this.el).find("#filter");
+ this.list = $(this.el).find("#joblist");
+ this.contextMenu = settings.contextMenuCallback;
+ this.listNodes = {};
+ },
+
+ filterJobs: function(self) {
+ var filter = this.filterInput.val();
+ // Clear all filters first
+ if (!filter || filter.trim() == "") {
+ this.unfilterAll(self);
+ return;
+ }
+
+ this.hideAll(self);
+ var showList = {};
+
+ // find the jobs that need to be exposed.
+ for (var key in this.listNodes) {
+ var li = this.listNodes[key];
+ var node = li.node;
+ var nodeName = node.id;
+ node.listElement = li;
+
+ var index = nodeName.indexOf(filter);
+ if (index == -1) {
+ continue;
+ }
+
+ var spanlabel = $(li).find("> a > span");
+
+ var endIndex = index + filter.length;
+ var newHTML = nodeName.substring(0, index) + "<span class=\"filterHighlight\">" +
+ nodeName.substring(index, endIndex) + "</span>" +
+ nodeName.substring(endIndex, nodeName.length);
+ $(spanlabel).html(newHTML);
+
+ // Apply classes to all the included embedded flows.
+ var pIndex = key.length;
+ while ((pIndex = key.lastIndexOf(":", pIndex - 1)) > 0) {
+ var parentId = key.substr(0, pIndex);
+ var parentLi = this.listNodes[parentId];
+ $(parentLi).show();
+ $(parentLi).addClass("subFilter");
+ }
+
+ $(li).show();
+ }
+ },
+
+ hideAll: function(self) {
+ for (var key in this.listNodes) {
+ var li = this.listNodes[key];
+ var label = $(li).find("> a > span");
+ $(label).text(li.node.id);
+ $(li).removeClass("subFilter");
+ $(li).hide();
+ }
+ },
+
+ unfilterAll: function(self) {
+ for (var key in this.listNodes) {
+ var li = this.listNodes[key];
+ var label = $(li).find("> a > span");
+ $(label).text(li.node.id);
+ $(li).removeClass("subFilter");
+ $(li).show();
+ }
+ },
+
+ handleStatusUpdate: function(evt) {
+ var data = this.model.get("data");
+ this.changeStatuses(data);
+ },
+
+ changeStatuses: function(data) {
+ for (var i = 0; i < data.nodes.length; ++i) {
+ var node = data.nodes[i];
+
+ // Confused? In updates, a node reference is given to the update node.
+ var liElement = node.listElement;
+ var child = $(liElement).children("a");
+ if (!$(child).hasClass(node.status)) {
+ $(child).removeClass(statusList.join(' '));
+ $(child).addClass(node.status);
+ $(child).attr("title", node.status + " (" + node.type + ")");
+ }
+ if (node.nodes) {
+ this.changeStatuses(node);
+ }
+ }
+ },
+
+ render: function(self) {
+ var data = this.model.get("data");
+ var nodes = data.nodes;
+
+ this.renderTree(this.list, data);
+
+ //this.assignInitialStatus(self);
+ this.handleDisabledChange(self);
+ this.changeStatuses(data);
+ },
+
+ renderTree: function(el, data, prefix) {
+ var nodes = data.nodes;
+ if (nodes.length == 0) {
+ console.log("No results");
+ return;
+ };
+ if (!prefix) {
+ prefix = "";
+ }
+
+ var nodeArray = nodes.slice(0);
+ nodeArray.sort(function(a, b) {
+ var diff = a.y - b.y;
+ if (diff == 0) {
+ return a.x - b.x;
+ }
+ else {
+ return diff;
+ }
+ });
+
+ var ul = document.createElement('ul');
+ $(ul).addClass("tree-list");
+ for (var i = 0; i < nodeArray.length; ++i) {
+ var li = document.createElement("li");
+ $(li).addClass("listElement");
+ $(li).addClass("tree-list-item");
+
+ // This is used for the filter step.
+ var listNodeName = prefix + nodeArray[i].id;
+ this.listNodes[listNodeName]=li;
+ li.node = nodeArray[i];
+ li.node.listElement = li;
+
+ var a = document.createElement("a");
+ var iconDiv = document.createElement('div');
+ $(iconDiv).addClass('icon');
+
+ $(a).append(iconDiv);
+
+ var span = document.createElement("span");
+ $(span).text(nodeArray[i].id);
+ $(span).addClass("jobname");
+ $(a).append(span);
+ $(li).append(a);
+ $(ul).append(li);
+
+ if (nodeArray[i].type == "flow") {
+ // Add the up down
+ var expandDiv = document.createElement("div");
+ $(expandDiv).addClass("expandarrow glyphicon glyphicon-chevron-down");
+ $(a).append(expandDiv);
+
+ // Create subtree
+ var subul = this.renderTree(li, nodeArray[i], listNodeName + ":");
+ $(subul).hide();
+ }
+ }
+
+ $(el).append(ul);
+ return ul;
+ },
+
+ handleMenuExpand: function(li) {
+ var expandArrow = $(li).find("> a > .expandarrow");
+ var submenu = $(li).find("> ul");
+
+ $(expandArrow).removeClass("glyphicon-chevron-down");
+ $(expandArrow).addClass("glyphicon-chevron-up");
+ $(submenu).slideDown();
+ },
+
+ handleMenuCollapse: function(li) {
+ var expandArrow = $(li).find("> a > .expandarrow");
+ var submenu = $(li).find("> ul");
+
+ $(expandArrow).removeClass("glyphicon-chevron-up");
+ $(expandArrow).addClass("glyphicon-chevron-down");
+ $(submenu).slideUp();
+ },
+
+ handleToggleMenuExpand: function(evt) {
+ var expandarrow = evt.currentTarget;
+ var li = $(evt.currentTarget).closest("li.listElement");
+ var submenu = $(li).find("> ul");
+
+ if ($(submenu).is(":visible")) {
+ this.handleMenuCollapse(li);
+ }
+ else {
+ this.handleMenuExpand(li);
+ }
+
+ evt.stopImmediatePropagation();
+ },
+
+ handleContextMenuClick: function(evt) {
+ if (this.contextMenu) {
+ this.contextMenu(evt, this.model, evt.currentTarget.node);
+ return false;
+ }
+ },
+
+ handleJobClick: function(evt) {
+ console.log("Job clicked");
+ var li = $(evt.currentTarget).closest("li.listElement");
+ var node = li[0].node;
+ if (!node) {
+ return;
+ }
+
+ if (this.model.has("selected")) {
+ var selected = this.model.get("selected");
+ if (selected == node) {
+ this.model.unset("selected");
+ }
+ else {
+ this.model.set({"selected": node});
+ }
+ }
+ else {
+ this.model.set({"selected": node});
+ }
+
+ evt.stopPropagation();
+ evt.cancelBubble = true;
+ },
+
+ handleDisabledChange: function(evt) {
+ this.changeDisabled(this.model.get('data'));
+ },
+
+ changeDisabled: function(data) {
+ for (var i =0; i < data.nodes; ++i) {
+ var node = data.nodes[i];
+ if (node.disabled = true) {
+ removeClass(node.listElement, "nodedisabled");
+ if (node.type=='flow') {
+ this.changeDisabled(node);
+ }
+ }
+ else {
+ addClass(node.listElement, "nodedisabled");
+ }
+ }
+ },
+
+ handleSelectionChange: function(evt) {
+ if (!this.model.hasChanged("selected")) {
+ return;
+ }
+
+ var previous = this.model.previous("selected");
+ var current = this.model.get("selected");
+
+ if (previous) {
+ $(previous.listElement).removeClass("active");
+ }
+
+ if (current) {
+ $(current.listElement).addClass("active");
+ this.propagateExpansion(current.listElement);
+ }
+ },
+
+ propagateExpansion: function(li) {
+ var li = $(li).parent().closest("li.listElement")[0];
+ if (li) {
+ this.propagateExpansion(li);
+ this.handleMenuExpand(li);
+ }
+ },
+
+ handleResetPanZoom: function(evt) {
+ this.model.trigger("resetPanZoom");
+ },
+
+ handleAutoPanZoom: function(evt) {
+ var target = evt.currentTarget;
+ if ($(target).hasClass('btn-default')) {
+ $(target).removeClass('btn-default');
+ $(target).addClass('btn-info');
+ }
+ else if ($(target).hasClass('btn-info')) {
+ $(target).removeClass('btn-info');
+ $(target).addClass('btn-default');
+ }
+
+ // Using $().hasClass('active') does not use here because it appears that
+ // this is called before the Bootstrap toggle completes.
+ this.model.set({"autoPanZoom": $(target).hasClass('btn-info')});
+ },
+
+ handleClose: function(evt) {
+ $("#joblist-panel").fadeOut();
+ },
+ handleOpen: function(evt) {
+ $("#joblist-panel").fadeIn();
+ }
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/login.js b/azkaban-webserver/src/web/js/azkaban/view/login.js
new file mode 100644
index 0000000..685153a
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/login.js
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var loginView;
+azkaban.LoginView = Backbone.View.extend({
+ events: {
+ "click #login-submit": "handleLogin",
+ 'keypress input': 'handleKeyPress'
+ },
+
+ initialize: function(settings) {
+ $('#error-msg').hide();
+ },
+
+ handleLogin: function(evt) {
+ console.log("Logging in.");
+ var username = $("#username").val();
+ var password = $("#password").val();
+
+ $.ajax({
+ async: "false",
+ url: contextURL,
+ dataType: "json",
+ type: "POST",
+ data: {
+ action: "login",
+ username: username,
+ password: password
+ },
+ success: function(data) {
+ if (data.error) {
+ $('#error-msg').text(data.error);
+ $('#error-msg').slideDown('fast');
+ }
+ else {
+ document.location.reload();
+ }
+ }
+ });
+ },
+
+ handleKeyPress: function(evt) {
+ if (evt.charCode == 13 || evt.keyCode == 13) {
+ this.handleLogin();
+ }
+ },
+
+ render: function() {
+ }
+});
+
+$(function() {
+ loginView = new azkaban.LoginView({el: $('#login-form')});
+});
azkaban-webserver/src/web/js/azkaban/view/main.js 209(+209 -0)
diff --git a/azkaban-webserver/src/web/js/azkaban/view/main.js b/azkaban-webserver/src/web/js/azkaban/view/main.js
new file mode 100644
index 0000000..8ba3851
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/main.js
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var projectTableView;
+azkaban.ProjectTableView = Backbone.View.extend({
+ events: {
+ "click .project-expander": "expandProject"
+ },
+
+ initialize: function(settings) {
+ },
+
+ expandProject: function(evt) {
+ if (evt.target.tagName == "A") {
+ return;
+ }
+
+ var target = evt.currentTarget;
+ var targetId = target.id;
+ var requestURL = contextURL + "/manager";
+
+ var targetExpanded = $('#' + targetId + '-child');
+ var targetTBody = $('#' + targetId + '-tbody');
+ var createFlowListFunction = this.createFlowListTable;
+
+ if (target.loading) {
+ console.log("Still loading.");
+ }
+ else if (target.loaded) {
+ if ($(targetExpanded).is(':visible')) {
+ $(target).addClass('expanded').removeClass('collapsed');
+ var expander = $(target).children('.project-expander-icon')[0];
+ $(expander).removeClass('glyphicon-chevron-up');
+ $(expander).addClass('glyphicon-chevron-down');
+ $(targetExpanded).slideUp(300);
+ }
+ else {
+ $(target).addClass('collapsed').removeClass('expanded');
+ var expander = $(target).children('.project-expander-icon')[0];
+ $(expander).removeClass('glyphicon-chevron-down');
+ $(expander).addClass('glyphicon-chevron-up');
+ $(targetExpanded).slideDown(300);
+ }
+ }
+ else {
+ // projectId is available
+ $(target).addClass('wait').removeClass('collapsed').removeClass('expanded');
+ target.loading = true;
+
+ var request = {
+ "project": targetId,
+ "ajax": "fetchprojectflows"
+ };
+
+ var successHandler = function(data) {
+ console.log("Success");
+ target.loaded = true;
+ target.loading = false;
+
+ createFlowListFunction(data, targetTBody);
+
+ $(target).addClass('collapsed').removeClass('wait');
+ var expander = $(target).children('.project-expander-icon')[0];
+ $(expander).removeClass('glyphicon-chevron-down');
+ $(expander).addClass('glyphicon-chevron-up');
+ $(targetExpanded).slideDown(300);
+ };
+
+ $.get(requestURL, request, successHandler, "json");
+ }
+ },
+
+ render: function() {
+ },
+
+ createFlowListTable: function(data, innerTable) {
+ var flows = data.flows;
+ flows.sort(function(a,b) {
+ return a.flowId.localeCompare(b.flowId);
+ });
+ var requestURL = contextURL + "/manager?project=" + data.project + "&flow=";
+ for (var i = 0; i < flows.length; ++i) {
+ var id = flows[i].flowId;
+ var ida = document.createElement("a");
+ ida.project = data.project;
+ $(ida).text(id);
+ $(ida).attr("href", requestURL + id);
+ $(ida).addClass('list-group-item');
+ $(innerTable).append(ida);
+ }
+ }
+});
+
+var projectHeaderView;
+azkaban.ProjectHeaderView = Backbone.View.extend({
+ events: {
+ "click #create-project-btn": "handleCreateProjectJob"
+ },
+
+ initialize: function(settings) {
+ console.log("project header view initialize.");
+ if (settings.errorMsg && settings.errorMsg != "null") {
+ $('#messaging').addClass("alert-danger");
+ $('#messaging').removeClass("alert-success");
+ $('#messaging-message').html(settings.errorMsg);
+ }
+ else if (settings.successMsg && settings.successMsg != "null") {
+ $('#messaging').addClass("alert-success");
+ $('#messaging').removeClass("alert-danger");
+ $('#messaging-message').html(settings.successMsg);
+ }
+ else {
+ $('#messaging').removeClass("alert-success");
+ $('#messaging').removeClass("alert-danger");
+ }
+ },
+
+ handleCreateProjectJob: function(evt) {
+ $('#create-project-modal').modal();
+ },
+
+ render: function() {
+ }
+});
+
+var createProjectView;
+azkaban.CreateProjectView = Backbone.View.extend({
+ events: {
+ "click #create-btn": "handleCreateProject"
+ },
+
+ initialize: function(settings) {
+ $("#modal-error-msg").hide();
+ },
+
+ handleCreateProject: function(evt) {
+ // First make sure we can upload
+ var projectName = $('#path').val();
+ var description = $('#description').val();
+ console.log("Creating");
+ $.ajax({
+ async: "false",
+ url: "manager",
+ dataType: "json",
+ type: "POST",
+ data: {
+ action: "create",
+ name: projectName,
+ description: description
+ },
+ success: function(data) {
+ if (data.status == "success") {
+ if (data.action == "redirect") {
+ window.location = data.path;
+ }
+ }
+ else {
+ if (data.action == "login") {
+ window.location = "";
+ }
+ else {
+ $("#modal-error-msg").text("ERROR: " + data.message);
+ $("#modal-error-msg").slideDown("fast");
+ }
+ }
+ }
+ });
+ },
+
+ render: function() {
+ }
+});
+
+var tableSorterView;
+$(function() {
+ projectHeaderView = new azkaban.ProjectHeaderView({
+ el: $('#create-project'),
+ successMsg: successMessage,
+ errorMsg: errorMessage
+ });
+
+ projectTableView = new azkaban.ProjectTableView({
+ el: $('#project-list')
+ });
+
+ /*tableSorterView = new azkaban.TableSorter({
+ el: $('#all-jobs'),
+ initialSort: $('.tb-name')
+ });*/
+
+ uploadView = new azkaban.CreateProjectView({
+ el: $('#create-project-modal')
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/message-dialog.js b/azkaban-webserver/src/web/js/azkaban/view/message-dialog.js
new file mode 100644
index 0000000..d13671f
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/message-dialog.js
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var messageDialogView;
+azkaban.MessageDialogView = Backbone.View.extend({
+ events: {
+ },
+
+ initialize: function(settings) {
+ },
+
+ show: function(title, message, callback) {
+ $("#azkaban-message-dialog-title").text(title);
+ $("#azkaban-message-dialog-text").text(message);
+ this.callback = callback;
+ $(this.el).on('hidden.bs.modal', function() {
+ if (callback) {
+ callback.call();
+ }
+ });
+ $(this.el).modal();
+ }
+});
+
+$(function() {
+ messageDialogView = new azkaban.MessageDialogView({
+ el: $('#azkaban-message-dialog')
+ });
+});
azkaban-webserver/src/web/js/azkaban/view/project.js 244(+244 -0)
diff --git a/azkaban-webserver/src/web/js/azkaban/view/project.js b/azkaban-webserver/src/web/js/azkaban/view/project.js
new file mode 100644
index 0000000..d076be9
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/project.js
@@ -0,0 +1,244 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var flowTableView;
+azkaban.FlowTableView = Backbone.View.extend({
+ events : {
+ "click .flow-expander": "expandFlowProject",
+ "mouseover .expanded-flow-job-list li": "highlight",
+ "mouseout .expanded-flow-job-list li": "unhighlight",
+ "click .runJob": "runJob",
+ "click .runWithDep": "runWithDep",
+ "click .execute-flow": "executeFlow",
+ "click .viewFlow": "viewFlow",
+ "click .viewJob": "viewJob"
+ },
+
+ initialize: function(settings) {
+ },
+
+ expandFlowProject: function(evt) {
+ if (evt.target.tagName == "A" || evt.target.tagName == "BUTTON") {
+ return;
+ }
+
+ var target = evt.currentTarget;
+ var targetId = target.id;
+ var requestURL = contextURL + "/manager";
+
+ var targetExpanded = $('#' + targetId + '-child');
+ var targetTBody = $('#' + targetId + '-tbody');
+
+ var createJobListFunction = this.createJobListTable;
+ if (target.loading) {
+ console.log("Still loading.");
+ }
+ else if (target.loaded) {
+ $(targetExpanded).collapse('toggle');
+ var expander = $(target).children('.flow-expander-icon')[0];
+ if ($(expander).hasClass('glyphicon-chevron-down')) {
+ $(expander).removeClass('glyphicon-chevron-down');
+ $(expander).addClass('glyphicon-chevron-up');
+ }
+ else {
+ $(expander).removeClass('glyphicon-chevron-up');
+ $(expander).addClass('glyphicon-chevron-down');
+ }
+ }
+ else {
+ // projectName is available
+ target.loading = true;
+ var requestData = {
+ "project": projectName,
+ "ajax": "fetchflowjobs",
+ "flow": targetId
+ };
+ var successHandler = function(data) {
+ console.log("Success");
+ target.loaded = true;
+ target.loading = false;
+ createJobListFunction(data, targetTBody);
+ $(targetExpanded).collapse('show');
+ var expander = $(target).children('.flow-expander-icon')[0];
+ $(expander).removeClass('glyphicon-chevron-down');
+ $(expander).addClass('glyphicon-chevron-up');
+ };
+ $.get(requestURL, requestData, successHandler, "json");
+ }
+ },
+
+ createJobListTable: function(data, innerTable) {
+ var nodes = data.nodes;
+ var flowId = data.flowId;
+ var project = data.project;
+ var requestURL = contextURL + "/manager?project=" + project + "&flow=" + flowId + "&job=";
+ for (var i = 0; i < nodes.length; i++) {
+ var job = nodes[i];
+ var name = job.id;
+ var level = job.level;
+ var nodeId = flowId + "-" + name;
+
+ var li = document.createElement('li');
+ $(li).addClass("list-group-item");
+ $(li).attr("id", nodeId);
+ li.flowId = flowId;
+ li.dependents = job.dependents;
+ li.dependencies = job.dependencies;
+ li.projectName = project;
+ li.jobName = name;
+
+ if (execAccess) {
+ var hoverMenuDiv = document.createElement('div');
+ $(hoverMenuDiv).addClass('pull-right');
+ $(hoverMenuDiv).addClass('job-buttons');
+
+ var divRunJob = document.createElement('button');
+ $(divRunJob).attr('type', 'button');
+ $(divRunJob).addClass("btn");
+ $(divRunJob).addClass("btn-success");
+ $(divRunJob).addClass("btn-xs");
+ $(divRunJob).addClass("runJob");
+ $(divRunJob).text("Run Job");
+ divRunJob.jobName = name;
+ divRunJob.flowId = flowId;
+ $(hoverMenuDiv).append(divRunJob);
+
+ var divRunWithDep = document.createElement("button");
+ $(divRunWithDep).attr('type', 'button');
+ $(divRunWithDep).addClass("btn");
+ $(divRunWithDep).addClass("btn-success");
+ $(divRunWithDep).addClass("btn-xs");
+ $(divRunWithDep).addClass("runWithDep");
+ $(divRunWithDep).text("Run With Dependencies");
+ divRunWithDep.jobName = name;
+ divRunWithDep.flowId = flowId;
+ $(hoverMenuDiv).append(divRunWithDep);
+
+ $(li).append(hoverMenuDiv);
+ }
+
+ var ida = document.createElement("a");
+ $(ida).css("margin-left", level * 20);
+ $(ida).attr("href", requestURL + name);
+ $(ida).text(name);
+
+ $(li).append(ida);
+ $(innerTable).append(li);
+ }
+ },
+
+ unhighlight: function(evt) {
+ var currentTarget = evt.currentTarget;
+ $(".dependent").removeClass("dependent");
+ $(".dependency").removeClass("dependency");
+ },
+
+ highlight: function(evt) {
+ var currentTarget = evt.currentTarget;
+ $(".dependent").removeClass("dependent");
+ $(".dependency").removeClass("dependency");
+ this.highlightJob(currentTarget);
+ },
+
+ highlightJob: function(currentTarget) {
+ var dependents = currentTarget.dependents;
+ var dependencies = currentTarget.dependencies;
+ var flowid = currentTarget.flowId;
+
+ if (dependents) {
+ for (var i = 0; i < dependents.length; ++i) {
+ var depId = flowid + "-" + dependents[i];
+ $("#"+depId).toggleClass("dependent");
+ }
+ }
+
+ if (dependencies) {
+ for (var i = 0; i < dependencies.length; ++i) {
+ var depId = flowid + "-" + dependencies[i];
+ $("#"+depId).toggleClass("dependency");
+ }
+ }
+ },
+
+ viewFlow: function(evt) {
+ console.log("View Flow");
+ var flowId = evt.currentTarget.flowId;
+ location.href = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
+ },
+
+ viewJob: function(evt) {
+ console.log("View Job");
+ var flowId = evt.currentTarget.flowId;
+ var jobId = evt.currentTarget.jobId;
+ location.href = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+ },
+
+ runJob: function(evt) {
+ console.log("Run Job");
+ var jobId = evt.currentTarget.jobName;
+ var flowId = evt.currentTarget.flowId;
+
+ var executingData = {
+ project: projectName,
+ ajax: "executeFlow",
+ flow: flowId,
+ job: jobId
+ };
+
+ this.executeFlowDialog(executingData);
+ },
+
+ runWithDep: function(evt) {
+ var jobId = evt.currentTarget.jobName;
+ var flowId = evt.currentTarget.flowId;
+ console.log("Run With Dep");
+
+ var executingData = {
+ project: projectName,
+ ajax: "executeFlow",
+ flow: flowId,
+ job: jobId,
+ withDep: true
+ };
+ this.executeFlowDialog(executingData);
+ },
+
+ executeFlow: function(evt) {
+ console.log("Execute Flow");
+ var flowId = $(evt.currentTarget).attr('flowid');
+
+ var executingData = {
+ project: projectName,
+ ajax: "executeFlow",
+ flow: flowId
+ };
+
+ this.executeFlowDialog(executingData);
+ },
+
+ executeFlowDialog: function(executingData) {
+ flowExecuteDialogView.show(executingData);
+ },
+
+ render: function() {
+ }
+});
+
+$(function() {
+ flowTableView = new azkaban.FlowTableView({el:$('#flow-tabs')});
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/project-logs.js b/azkaban-webserver/src/web/js/azkaban/view/project-logs.js
new file mode 100644
index 0000000..f4bd5c3
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/project-logs.js
@@ -0,0 +1,137 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var logModel;
+azkaban.LogModel = Backbone.Model.extend({});
+
+// From ProjectLogEvent.java
+// ERROR(128), CREATED(1), DELETED(2), USER_PERMISSION(3), GROUP_PERMISSION(4), DESCRIPTION(5);
+var typeMapping = {
+ "ERROR": "Error",
+ "CREATED": "Project Created",
+ "DELETED": "Project Deleted",
+ "USER_PERMISSION" : "User Permission",
+ "GROUP_PERMISSION" : "Group Permission",
+ "DESCRIPTION" : "Description Set",
+ "SCHEDULE": "Schedule",
+ "UPLOADED": "Uploaded"
+};
+
+var projectLogView;
+azkaban.ProjectLogView = Backbone.View.extend({
+ events: {
+ "click #updateLogBtn": "handleUpdate"
+ },
+
+ initialize: function(settings) {
+ this.model.set({"current": 0});
+ this.handleUpdate();
+ },
+
+ handleUpdate: function(evt) {
+ var current = this.model.get("current");
+ var requestURL = contextURL + "/manager";
+ var model = this.model;
+ var requestData = {
+ "project": projectName,
+ "ajax": "fetchProjectLogs",
+ "size": 1000,
+ "skip": 0
+ };
+
+ var successHandler = function(data) {
+ console.log("fetchLogs");
+ if (data.error) {
+ showDialog("Error", data.error);
+ return;
+ }
+ // Get the columns to map to the values.
+ var columns = data.columns;
+ var columnMap = {};
+ for (var i =0; i < columns.length; ++i) {
+ columnMap[columns[i]] = i;
+ }
+ var logSection = $("#logTable").find("tbody")[0];
+ $(logSection).empty();
+ var logData = data.logData;
+ for (var i = 0; i < logData.length; ++i) {
+ var event = logData[i];
+ var user = event[columnMap['user']];
+ var time = event[columnMap['time']];
+ var type = event[columnMap['type']];
+ var message = event[columnMap['message']];
+
+ var containerEvent = document.createElement("tr");
+ $(containerEvent).addClass("projectEvent");
+
+ var containerTime = document.createElement("td");
+ $(containerTime).addClass("time");
+ $(containerTime).text(getDateFormat(new Date(time)));
+
+ var containerUser = document.createElement("td");
+ $(containerUser).addClass("user");
+ $(containerUser).text(user);
+
+ var containerType = document.createElement("td");
+ $(containerType).addClass("type");
+ $(containerType).addClass(type);
+ $(containerType).text(typeMapping[type] ? typeMapping[type] : type);
+
+ var containerMessage = document.createElement("td");
+ $(containerMessage).addClass("message");
+ $(containerMessage).text(message);
+
+ $(containerEvent).append(containerTime);
+ $(containerEvent).append(containerUser);
+ $(containerEvent).append(containerType);
+ $(containerEvent).append(containerMessage);
+
+ $(logSection).append(containerEvent);
+ }
+
+ model.set({"log": data});
+ };
+ $.get(requestURL, requestData, successHandler);
+ }
+});
+
+var showDialog = function(title, message) {
+ $('#messageTitle').text(title);
+
+ $('#messageBox').text(message);
+
+ $('#messageDialog').modal({
+ closeHTML: "<a href='#' title='Close' class='modal-close'>x</a>",
+ position: ["20%",],
+ containerId: 'confirm-container',
+ containerCss: {
+ 'height': '220px',
+ 'width': '565px'
+ },
+ onShow: function (dialog) {
+ }
+ });
+}
+
+
+$(function() {
+ var selected;
+
+ logModel = new azkaban.LogModel();
+ projectLogView = new azkaban.ProjectLogView({el:$('#projectLogView'), model: logModel});
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/project-modals.js b/azkaban-webserver/src/web/js/azkaban/view/project-modals.js
new file mode 100644
index 0000000..302cbfe
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/project-modals.js
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var projectView;
+azkaban.ProjectView = Backbone.View.extend({
+ events: {
+ "click #project-upload-btn": "handleUploadProjectJob",
+ "click #project-delete-btn": "handleDeleteProject"
+ },
+
+ initialize: function(settings) {
+ },
+
+ handleUploadProjectJob: function(evt) {
+ console.log("click upload project");
+ $('#upload-project-modal').modal();
+ },
+
+ handleDeleteProject: function(evt) {
+ console.log("click delete project");
+ $('#delete-project-modal').modal();
+ },
+
+ render: function() {
+ }
+});
+
+var uploadProjectView;
+azkaban.UploadProjectView = Backbone.View.extend({
+ events: {
+ "click #upload-project-btn": "handleCreateProject"
+ },
+
+ initialize: function(settings) {
+ console.log("Hide upload project modal error msg");
+ $("#upload-project-modal-error-msg").hide();
+ },
+
+ handleCreateProject: function(evt) {
+ console.log("Upload project button.");
+ $("#upload-project-form").submit();
+ },
+
+ render: function() {
+ }
+});
+
+var deleteProjectView;
+azkaban.DeleteProjectView = Backbone.View.extend({
+ events: {
+ "click #delete-btn": "handleDeleteProject"
+ },
+
+ initialize: function(settings) {
+ },
+
+ handleDeleteProject: function(evt) {
+ $("#delete-form").submit();
+ },
+
+ render: function() {
+ }
+});
+
+var projectDescription;
+azkaban.ProjectDescriptionView = Backbone.View.extend({
+ events: {
+ "click #project-description": "handleDescriptionEdit",
+ "click #project-description-btn": "handleDescriptionSave"
+ },
+
+ initialize: function(settings) {
+ console.log("project description initialize");
+ },
+
+ handleDescriptionEdit: function(evt) {
+ console.log("Edit description");
+ var description = null;
+ if ($('#project-description').hasClass('editable-placeholder')) {
+ description = '';
+ $('#project-description').removeClass('editable-placeholder');
+ }
+ else {
+ description = $('#project-description').text();
+ }
+ $('#project-description-edit').attr("value", description);
+ $('#project-description').hide();
+ $('#project-description-form').show();
+ },
+
+ handleDescriptionSave: function(evt) {
+ var newText = $('#project-description-edit').val();
+ if ($('#project-description-edit').hasClass('has-error')) {
+ $('#project-description-edit').removeClass('has-error');
+ }
+ var requestURL = contextURL + "/manager";
+ var requestData = {
+ "project": projectName,
+ "ajax":"changeDescription",
+ "description": newText
+ };
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#project-description-edit').addClass('has-error');
+ alert(data.error);
+ return;
+ }
+ $('#project-description-form').hide();
+ if (newText != '') {
+ $('#project-description').text(newText);
+ }
+ else {
+ $('#project-description').text('Add project description.');
+ $('#project-description').addClass('editable-placeholder');
+ }
+ $('#project-description').show();
+ };
+ $.get(requestURL, requestData, successHandler, "json");
+ },
+
+ render: function() {
+ }
+});
+
+$(function() {
+ projectView = new azkaban.ProjectView({
+ el: $('#project-options')
+ });
+ uploadView = new azkaban.UploadProjectView({
+ el: $('#upload-project-modal')
+ });
+ deleteProjectView = new azkaban.DeleteProjectView({
+ el: $('#delete-project-modal')
+ });
+ projectDescription = new azkaban.ProjectDescriptionView({
+ el: $('#project-sidebar')
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/project-permissions.js b/azkaban-webserver/src/web/js/azkaban/view/project-permissions.js
new file mode 100644
index 0000000..2cdce28
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/project-permissions.js
@@ -0,0 +1,354 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var permissionTableView;
+var groupPermissionTableView;
+
+azkaban.PermissionTableView = Backbone.View.extend({
+ events : {
+ "click button": "handleChangePermission"
+ },
+
+ initialize : function(settings) {
+ this.group = settings.group;
+ this.proxy = settings.proxy;
+ },
+
+ render: function() {
+ },
+
+ handleChangePermission: function(evt) {
+ var currentTarget = evt.currentTarget;
+ changePermissionView.display(currentTarget.id, false, this.group, this.proxy);
+ }
+});
+
+var proxyTableView;
+azkaban.ProxyTableView= Backbone.View.extend({
+ events : {
+ "click button": "handleRemoveProxy"
+ },
+
+ initialize : function(settings) {
+ },
+
+ render: function() {
+ },
+
+ handleRemoveProxy: function(evt) {
+ removeProxyView.display($(evt.currentTarget).attr("name"));
+ }
+});
+
+var removeProxyView;
+azkaban.RemoveProxyView = Backbone.View.extend({
+ events: {
+ "click #remove-proxy-btn": "handleRemoveProxy"
+ },
+
+ initialize : function(settings) {
+ $('#remove-proxy-error-msg').hide();
+ },
+
+ display: function(proxyName) {
+ this.el.proxyName = proxyName;
+ $("#remove-proxy-msg").text("Removing proxy user '" + proxyName + "'");
+ $(this.el).modal().on('hide.bs.modal', function(e) {
+ $('#remove-proxy-error-msg').hide();
+ });
+ },
+
+ handleRemoveProxy: function() {
+ var requestURL = contextURL + "/manager";
+ var proxyName = this.el.proxyName;
+ var requestData = {
+ "project": projectName,
+ "name": proxyName,
+ "ajax": "removeProxyUser"
+ };
+ var successHandler = function(data) {
+ console.log("Output");
+ if (data.error) {
+ $("#remove-proxy-error-msg").text(data.error);
+ $("#remove-proxy-error-msg").slideDown();
+ return;
+ }
+ var replaceURL = requestURL + "?project=" + projectName +"&permissions";
+ window.location.replace(replaceURL);
+ };
+
+ $.get(requestURL, requestData, successHandler, "json");
+ }
+});
+
+var addProxyView;
+azkaban.AddProxyView = Backbone.View.extend({
+ events: {
+ "click #add-proxy-btn": "handleAddProxy"
+ },
+
+ initialize : function(settings) {
+ $('#add-proxy-error-msg').hide();
+ },
+
+ display: function() {
+ $(this.el).modal().on('hide.bs.modal', function(e) {
+ $('#add-proxy-error-msg').hide();
+ });
+ },
+
+ handleAddProxy: function() {
+ var requestURL = contextURL + "/manager";
+ var name = $('#proxy-user-box').val().trim();
+ var requestData = {
+ "project": projectName,
+ "name": name,
+ "ajax":"addProxyUser"
+ };
+
+ var successHandler = function(data) {
+ console.log("Output");
+ if (data.error) {
+ $("#add-proxy-error-msg").text(data.error);
+ $("#add-proxy-error-msg").slideDown();
+ return;
+ }
+
+ var replaceURL = requestURL + "?project=" + projectName +"&permissions";
+ window.location.replace(replaceURL);
+ };
+ $.get(requestURL, requestData, successHandler, "json");
+ }
+});
+
+var changePermissionView;
+azkaban.ChangePermissionView= Backbone.View.extend({
+ events: {
+ "click input[type=checkbox]": "handleCheckboxClick",
+ "click #change-btn": "handleChangePermissions"
+ },
+
+ initialize: function(settings) {
+ $('#change-permission-error-msg').hide();
+ },
+
+ display: function(userid, newPerm, group, proxy) {
+ // 6 is the length of the prefix "group-"
+ this.userid = group ? userid.substring(6, userid.length) : userid;
+ if(group == true) {
+ this.userid = userid.substring(6, userid.length)
+ } else if (proxy == true) {
+ this.userid = userid.substring(6, userid.length)
+ } else {
+ this.userid = userid
+ }
+
+ this.permission = {};
+ $('#user-box').val(this.userid);
+ this.newPerm = newPerm;
+ this.group = group;
+
+ var prefix = userid;
+ var adminInput = $("#" + prefix + "-admin-checkbox");
+ var readInput = $("#" + prefix + "-read-checkbox");
+ var writeInput = $("#" + prefix + "-write-checkbox");
+ var executeInput = $("#" + prefix + "-execute-checkbox");
+ var scheduleInput = $("#" + prefix + "-schedule-checkbox");
+
+ if (newPerm) {
+ if (group) {
+ $('#change-title').text("Add New Group Permissions");
+ }
+ else if(proxy){
+ $('#change-title').text("Add New Proxy User Permissions");
+ }
+ else{
+ $('#change-title').text("Add New User Permissions");
+ }
+ $('#user-box').attr("disabled", null);
+
+ // default
+ this.permission.admin = false;
+ this.permission.read = true;
+ this.permission.write = false;
+ this.permission.execute = false;
+ this.permission.schedule = false;
+ }
+ else {
+ if (group) {
+ $('#change-title').text("Change Group Permissions");
+ }
+ else {
+ $('#change-title').text("Change User Permissions");
+ }
+
+ $('#user-box').attr("disabled", "disabled");
+
+ this.permission.admin = $(adminInput).is(":checked");
+ this.permission.read = $(readInput).is(":checked");
+ this.permission.write = $(writeInput).is(":checked");
+ this.permission.execute = $(executeInput).is(":checked");
+ this.permission.schedule = $(scheduleInput).is(":checked");
+ }
+
+ this.changeCheckbox();
+
+ changePermissionView.render();
+ $('#change-permission').modal().on('hide.bs.modal', function(e) {
+ $('#change-permission-error-msg').hide();
+ });
+ },
+
+ render: function() {
+ },
+
+ handleCheckboxClick: function(evt) {
+ console.log("click");
+ var targetName = evt.currentTarget.name;
+ if(targetName == "proxy") {
+ this.doProxy = evt.currentTarget.checked;
+ }
+ else {
+ this.permission[targetName] = evt.currentTarget.checked;
+ }
+ this.changeCheckbox(evt);
+ },
+
+ changeCheckbox: function(evt) {
+ var perm = this.permission;
+
+ if (perm.admin) {
+ $("#admin-change").attr("checked", true);
+ $("#read-change").attr("checked", true);
+ $("#read-change").attr("disabled", "disabled");
+
+ $("#write-change").attr("checked", true);
+ $("#write-change").attr("disabled", "disabled");
+
+ $("#execute-change").attr("checked", true);
+ $("#execute-change").attr("disabled", "disabled");
+
+ $("#schedule-change").attr("checked", true);
+ $("#schedule-change").attr("disabled", "disabled");
+ }
+ else {
+ $("#admin-change").attr("checked", false);
+
+ $("#read-change").attr("checked", perm.read);
+ $("#read-change").attr("disabled", null);
+
+ $("#write-change").attr("checked", perm.write);
+ $("#write-change").attr("disabled", null);
+
+ $("#execute-change").attr("checked", perm.execute);
+ $("#execute-change").attr("disabled", null);
+
+ $("#schedule-change").attr("checked", perm.schedule);
+ $("#schedule-change").attr("disabled", null);
+ }
+
+ $("#change-btn").removeClass("btn-disabled");
+ $("#change-btn").attr("disabled", null);
+
+ if (perm.admin || perm.read || perm.write || perm.execute || perm.schedule) {
+ $("#change-btn").text("Commit");
+ }
+ else {
+ if (this.newPerm) {
+ $("#change-btn").disabled = true;
+ $("#change-btn").addClass("btn-disabled");
+ }
+ else {
+ $("#change-btn").text("Remove");
+ }
+ }
+ },
+
+ handleChangePermissions : function(evt) {
+ var requestURL = contextURL + "/manager";
+ var name = $('#user-box').val().trim();
+ var command = this.newPerm ? "addPermission" : "changePermission";
+ var group = this.group;
+
+ var permission = {};
+ permission.admin = $("#admin-change").is(":checked");
+ permission.read = $("#read-change").is(":checked");
+ permission.write = $("#write-change").is(":checked");
+ permission.execute = $("#execute-change").is(":checked");
+ permission.schedule = $("#schedule-change").is(":checked");
+
+ var requestData = {
+ "project": projectName,
+ "name": name,
+ "ajax": command,
+ "permissions": this.permission,
+ "group": group
+ };
+ var successHandler = function(data) {
+ console.log("Output");
+ if (data.error) {
+ $("#change-permission-error-msg").text(data.error);
+ $("#change-permission-error-msg").slideDown();
+ return;
+ }
+
+ var replaceURL = requestURL + "?project=" + projectName +"&permissions";
+ window.location.replace(replaceURL);
+ };
+
+ $.get(requestURL, requestData, successHandler, "json");
+ }
+});
+
+$(function() {
+ permissionTableView = new azkaban.PermissionTableView({
+ el: $('#permissions-table'),
+ group: false,
+ proxy: false
+ });
+ groupPermissionTableView = new azkaban.PermissionTableView({
+ el: $('#group-permissions-table'),
+ group: true,
+ proxy: false
+ });
+ proxyTableView = new azkaban.ProxyTableView({
+ el: $('#proxy-user-table'),
+ group: false,
+ proxy: true
+ });
+ changePermissionView = new azkaban.ChangePermissionView({
+ el: $('#change-permission')
+ });
+ addProxyView = new azkaban.AddProxyView({
+ el: $('#add-proxy')
+ });
+ removeProxyView = new azkaban.RemoveProxyView({
+ el: $('#remove-proxy')
+ });
+ $('#addUser').bind('click', function() {
+ changePermissionView.display("", true, false, false);
+ });
+
+ $('#addGroup').bind('click', function() {
+ changePermissionView.display("", true, true, false);
+ });
+
+ $('#addProxyUser').bind('click', function() {
+ addProxyView.display();
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/scheduled.js b/azkaban-webserver/src/web/js/azkaban/view/scheduled.js
new file mode 100644
index 0000000..d470aac
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/scheduled.js
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var slaView;
+var tableSorterView;
+$(function() {
+ slaView = new azkaban.ChangeSlaView({el:$('#sla-options')});
+ tableSorterView = new azkaban.TableSorter({el:$('#scheduledFlowsTbl')});
+ //var requestURL = contextURL + "/manager";
+
+ // Set up the Flow options view. Create a new one every time :p
+ //$('#addSlaBtn').click( function() {
+ // slaView.show();
+ //});
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js b/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
new file mode 100644
index 0000000..13da163
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+var schedulePanelView;
+azkaban.SchedulePanelView = Backbone.View.extend({
+ events: {
+ "click #schedule-button": "scheduleFlow"
+ },
+
+ initialize: function(settings) {
+ $("#timepicker").datetimepicker({pickDate: false});
+ $("#datepicker").datetimepicker({pickTime: false});
+ },
+
+ render: function() {
+ },
+
+ showSchedulePanel: function() {
+ $('#schedule-modal').modal();
+ },
+
+ hideSchedulePanel: function() {
+ $('#schedule-modal').modal("hide");
+ },
+
+ scheduleFlow: function() {
+ var timeVal = $('#timepicker').val();
+ var timezoneVal = $('#timezone').val();
+
+ var dateVal = $('#datepicker').val();
+
+ var is_recurringVal = $('#is_recurring').val();
+ var periodVal = $('#period').val();
+ var periodUnits = $('#period_units').val();
+
+ var scheduleURL = contextURL + "/schedule"
+ var scheduleData = flowExecuteDialogView.getExecutionOptionData();
+
+ console.log("Creating schedule for " + projectName + "." +
+ scheduleData.flow);
+
+ var scheduleTime = moment(timeVal, 'h/mm A').format('h,mm,A,') + timezoneVal;
+ console.log(scheduleTime);
+
+ var scheduleDate = $('#datepicker').val();
+ var is_recurring = document.getElementById('is_recurring').checked
+ ? 'on' : 'off';
+ var period = $('#period').val() + $('#period_units').val();
+
+ scheduleData.ajax = "scheduleFlow";
+ scheduleData.projectName = projectName;
+ scheduleData.scheduleTime = scheduleTime;
+ scheduleData.scheduleDate = scheduleDate;
+ scheduleData.is_recurring = is_recurring;
+ scheduleData.period = period;
+
+ var successHandler = function(data) {
+ if (data.error) {
+ schedulePanelView.hideSchedulePanel();
+ messageDialogView.show("Error Scheduling Flow", data.message);
+ }
+ else {
+ schedulePanelView.hideSchedulePanel();
+ messageDialogView.show("Flow Scheduled", data.message, function() {
+ window.location.href = scheduleURL;
+ });
+ }
+ };
+
+ $.post(scheduleURL, scheduleData, successHandler, "json");
+ }
+});
+
+$(function() {
+ schedulePanelView = new azkaban.SchedulePanelView({
+ el: $('#schedule-modal')
+ });
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/schedule-sla.js b/azkaban-webserver/src/web/js/azkaban/view/schedule-sla.js
new file mode 100644
index 0000000..cffffca
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/schedule-sla.js
@@ -0,0 +1,292 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.ChangeSlaView = Backbone.View.extend({
+ events: {
+ "click": "closeEditingTarget",
+ "click #set-sla-btn": "handleSetSla",
+ "click #remove-sla-btn": "handleRemoveSla",
+ "click #add-btn": "handleAddRow"
+ },
+
+ initialize: function(setting) {
+ $('#sla-options').on('hidden.bs.modal', function() {
+ slaView.handleSlaCancel();
+ });
+ },
+
+ handleSlaCancel: function() {
+ console.log("Clicked cancel button");
+ var scheduleURL = contextURL + "/schedule";
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ var rows = tFlowRules.rows;
+ var rowLength = rows.length
+ for (var i = 0; i < rowLength-1; i++) {
+ tFlowRules.deleteRow(0);
+ }
+ },
+
+ initFromSched: function(scheduleId, flowName) {
+ this.scheduleId = scheduleId;
+ var scheduleURL = contextURL + "/schedule"
+ this.scheduleURL = scheduleURL;
+
+ var indexToName = {};
+ var nameToIndex = {};
+ var indexToText = {};
+ this.indexToName = indexToName;
+ this.nameToIndex = nameToIndex;
+ this.indexToText = indexToText;
+
+ var ruleBoxOptions = ["SUCCESS", "FINISH"];
+ this.ruleBoxOptions = ruleBoxOptions;
+
+ var fetchScheduleData = {
+ "scheduleId": this.scheduleId,
+ "ajax": "slaInfo"
+ };
+
+ var successHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ return;
+ }
+ if (data.slaEmails) {
+ $('#slaEmails').val(data.slaEmails.join());
+ }
+
+ var allJobNames = data.allJobNames;
+
+ indexToName[0] = "";
+ nameToIndex[flowName] = 0;
+ indexToText[0] = "flow " + flowName;
+ for (var i = 1; i <= allJobNames.length; i++) {
+ indexToName[i] = allJobNames[i-1];
+ nameToIndex[allJobNames[i-1]] = i;
+ indexToText[i] = "job " + allJobNames[i-1];
+ }
+
+ // populate with existing settings
+ if (data.settings) {
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ for (var setting in data.settings) {
+ var rFlowRule = tFlowRules.insertRow(0);
+
+ var cId = rFlowRule.insertCell(-1);
+ var idSelect = document.createElement("select");
+ idSelect.setAttribute("class", "form-control");
+ for (var i in indexToName) {
+ idSelect.options[i] = new Option(indexToText[i], indexToName[i]);
+ if (data.settings[setting].id == indexToName[i]) {
+ idSelect.options[i].selected = true;
+ }
+ }
+ cId.appendChild(idSelect);
+
+ var cRule = rFlowRule.insertCell(-1);
+ var ruleSelect = document.createElement("select");
+ ruleSelect.setAttribute("class", "form-control");
+ for (var i in ruleBoxOptions) {
+ ruleSelect.options[i] = new Option(ruleBoxOptions[i], ruleBoxOptions[i]);
+ if (data.settings[setting].rule == ruleBoxOptions[i]) {
+ ruleSelect.options[i].selected = true;
+ }
+ }
+ cRule.appendChild(ruleSelect);
+
+ var cDuration = rFlowRule.insertCell(-1);
+ var duration = document.createElement("input");
+ duration.type = "text";
+ duration.setAttribute("class", "form-control durationpick");
+ var rawMinutes = data.settings[setting].duration;
+ var intMinutes = rawMinutes.substring(0, rawMinutes.length-1);
+ var minutes = parseInt(intMinutes);
+ var hours = Math.floor(minutes / 60);
+ minutes = minutes % 60;
+ duration.value = hours + ":" + minutes;
+ cDuration.appendChild(duration);
+
+ var cEmail = rFlowRule.insertCell(-1);
+ var emailCheck = document.createElement("input");
+ emailCheck.type = "checkbox";
+ for (var act in data.settings[setting].actions) {
+ if (data.settings[setting].actions[act] == "EMAIL") {
+ emailCheck.checked = true;
+ }
+ }
+ cEmail.appendChild(emailCheck);
+
+ var cKill = rFlowRule.insertCell(-1);
+ var killCheck = document.createElement("input");
+ killCheck.type = "checkbox";
+ for (var act in data.settings[setting].actions) {
+ if (data.settings[setting].actions[act] == "KILL") {
+ killCheck.checked = true;
+ }
+ }
+ cKill.appendChild(killCheck);
+ $('.durationpick').datetimepicker({
+ pickDate: false,
+ use24hours: true
+ });
+ }
+ }
+ $('.durationpick').datetimepicker({
+ pickDate: false,
+ use24hours: true
+ });
+ };
+
+ $.get(this.scheduleURL, fetchScheduleData, successHandler, "json");
+
+ $('#sla-options').modal();
+
+ //this.schedFlowOptions = sched.flowOptions
+ console.log("Loaded schedule info. Ready to set SLA.");
+ },
+
+ handleRemoveSla: function(evt) {
+ console.log("Clicked remove sla button");
+ var scheduleURL = this.scheduleURL;
+ var redirectURL = this.scheduleURL;
+ var requestData = {
+ "action": "removeSla",
+ "scheduleId": this.scheduleId
+ };
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#errorMsg').text(data.error)
+ }
+ else {
+ window.location = redirectURL
+ }
+ };
+ $.post(scheduleURL, requestData, successHanlder, "json");
+ },
+
+ handleSetSla: function(evt) {
+ var slaEmails = $('#slaEmails').val();
+ var settings = {};
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ for (var row = 0; row < tFlowRules.rows.length-1; row++) {
+ var rFlowRule = tFlowRules.rows[row];
+ var id = rFlowRule.cells[0].firstChild.value;
+ var rule = rFlowRule.cells[1].firstChild.value;
+ var duration = rFlowRule.cells[2].firstChild.value;
+ var email = rFlowRule.cells[3].firstChild.checked;
+ var kill = rFlowRule.cells[4].firstChild.checked;
+ settings[row] = id + "," + rule + "," + duration + "," + email + "," + kill;
+ }
+
+ var slaData = {
+ scheduleId: this.scheduleId,
+ ajax: "setSla",
+ slaEmails: slaEmails,
+ settings: settings
+ };
+
+ var scheduleURL = this.scheduleURL;
+ var successHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ }
+ else {
+ tFlowRules.length = 0;
+ window.location = scheduleURL;
+ }
+ };
+ $.post(scheduleURL, slaData, successHandler, "json");
+ },
+
+ handleAddRow: function(evt) {
+ var indexToName = this.indexToName;
+ var nameToIndex = this.nameToIndex;
+ var indexToText = this.indexToText;
+ var ruleBoxOptions = this.ruleBoxOptions;
+
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ var rFlowRule = tFlowRules.insertRow(tFlowRules.rows.length-1);
+
+ var cId = rFlowRule.insertCell(-1);
+ var idSelect = document.createElement("select");
+ idSelect.setAttribute("class", "form-control");
+ for (var i in indexToName) {
+ idSelect.options[i] = new Option(indexToText[i], indexToName[i]);
+ }
+ cId.appendChild(idSelect);
+
+ var cRule = rFlowRule.insertCell(-1);
+ var ruleSelect = document.createElement("select");
+ ruleSelect.setAttribute("class", "form-control");
+ for (var i in ruleBoxOptions) {
+ ruleSelect.options[i] = new Option(ruleBoxOptions[i], ruleBoxOptions[i]);
+ }
+ cRule.appendChild(ruleSelect);
+
+ var cDuration = rFlowRule.insertCell(-1);
+ var duration = document.createElement("input");
+ duration.type = "text";
+ duration.setAttribute("class", "durationpick form-control");
+ cDuration.appendChild(duration);
+
+ var cEmail = rFlowRule.insertCell(-1);
+ var emailCheck = document.createElement("input");
+ emailCheck.type = "checkbox";
+ cEmail.appendChild(emailCheck);
+
+ var cKill = rFlowRule.insertCell(-1);
+ var killCheck = document.createElement("input");
+ killCheck.type = "checkbox";
+ cKill.appendChild(killCheck);
+
+ $('.durationpick').datetimepicker({
+ pickDate: false,
+ use24hours: true
+ });
+ return rFlowRule;
+ },
+
+ handleEditColumn: function(evt) {
+ var curTarget = evt.currentTarget;
+ if (this.editingTarget != curTarget) {
+ this.closeEditingTarget();
+
+ var text = $(curTarget).children(".spanValue").text();
+ $(curTarget).empty();
+
+ var input = document.createElement("input");
+ $(input).attr("type", "text");
+ $(input).css("width", "100%");
+ $(input).val(text);
+ $(curTarget).addClass("editing");
+ $(curTarget).append(input);
+ $(input).focus();
+ this.editingTarget = curTarget;
+ }
+ },
+
+ handleRemoveColumn: function(evt) {
+ var curTarget = evt.currentTarget;
+ // Should be the table
+ var row = curTarget.parentElement.parentElement;
+ $(row).remove();
+ },
+
+ closeEditingTarget: function(evt) {
+ }
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/table-sort.js b/azkaban-webserver/src/web/js/azkaban/view/table-sort.js
new file mode 100644
index 0000000..968b7af
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/table-sort.js
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.TableSorter = Backbone.View.extend({
+ events: {
+ "click .sortable": "handleClickSort"
+ },
+
+ initialize: function(settings) {
+ $(this.el).addClass("sortableTable");
+
+ var thead = $(this.el).children("thead");
+ var th = $(thead).find("th");
+
+ $(th).addClass("sortable");
+ $("th.ignoresort").removeClass("sortable");
+ var sortDiv = document.createElement("div");
+
+ $(sortDiv).addClass("sortIcon");
+
+ $(th).append(sortDiv);
+
+ var tbody = $(this.el).children("tbody");
+ var rows = $(tbody).children("tr");
+
+ var row;
+ for (var i = 0; i < rows.length; ++i ) {
+ var nextRow = rows[i];
+ if (row && $(nextRow).hasClass("childrow")) {
+ if (!row.childRows) {
+ row.childRows = new Array();
+ }
+ row.childRows.push(nextRow);
+ }
+ else {
+ row = nextRow;
+ }
+ }
+
+ if (settings.initialSort) {
+ this.toggleSort(settings.initialSort);
+ }
+ },
+
+ handleClickSort: function(evt) {
+ this.toggleSort(evt.currentTarget);
+ },
+
+ toggleSort: function(th) {
+ console.log("sorting by index " + $(th).index());
+ if ($(th).hasClass("asc")) {
+ $(th).removeClass("asc");
+ $(th).addClass("desc");
+ // Sort to descending
+
+ this.sort($(th).index(), true);
+ }
+ else if ($(th).hasClass("desc")) {
+ $(th).removeClass("desc");
+ $(th).addClass("asc");
+
+ this.sort($(th).index(), false);
+ }
+ else {
+ $(th).parent().children(".sortable").removeClass("asc").removeClass("desc");
+ $(th).addClass("asc");
+
+ this.sort($(th).index(), false);
+ }
+ },
+
+ sort: function(index, desc) {
+ var tbody = $(this.el).children("tbody");
+ var rows = $(tbody).children("tr");
+
+ var tdToSort = new Array();
+ for (var i = 0; i < rows.length; ++i) {
+ var row = rows[i];
+ if (!$(row).hasClass("childrow")) {
+ var td = row.children[index];
+ tdToSort.push(td);
+ }
+ }
+
+ if (desc) {
+ tdToSort.sort(function(a,b) {
+ var texta = $(a).text().trim().toLowerCase();
+ var textb = $(b).text().trim().toLowerCase();
+
+ if (texta < textb) {
+ return 1;
+ }
+ else if (texta > textb) {
+ return -1;
+ }
+ else {
+ return 0;
+ }
+ });
+ }
+ else {
+ tdToSort.sort(function(a,b) {
+ var texta = $(a).text().trim().toLowerCase();
+ var textb = $(b).text().trim().toLowerCase();
+
+ if (texta < textb) {
+ return -1;
+ }
+ else if (texta > textb) {
+ return 1;
+ }
+ else {
+ return 0;
+ }
+ });
+ }
+
+ var sortedTR = new Array();
+ for (var i = 0; i < tdToSort.length; ++i) {
+ var tr = $(tdToSort[i]).parent();
+ sortedTR.push(tr);
+
+ var childRows = tr[0].childRows;
+ if (childRows) {
+ for(var j=0; j < childRows.length; ++j) {
+ sortedTR.push(childRows[j]);
+ }
+ }
+ }
+
+ for (var i = 0; i < sortedTR.length; ++i) {
+ $(tbody).append(sortedTR[i]);
+ }
+ },
+
+ render: function() {
+ console.log("render sorted table");
+ }
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/time-graph.js b/azkaban-webserver/src/web/js/azkaban/view/time-graph.js
new file mode 100644
index 0000000..a74a49c
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/time-graph.js
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+azkaban.TimeGraphView = Backbone.View.extend({
+ events: {
+ },
+
+ initialize: function(settings) {
+ this.model.bind('render', this.render, this);
+ this.model.bind('change:page', this.render, this);
+ this.modelField = settings.modelField;
+ this.graphContainer = settings.el;
+ this.render();
+ },
+
+ render: function(self) {
+ var series = this.model.get(this.modelField);
+ if (series == null) {
+ return;
+ }
+
+ // Array of points to be passed to Morris.
+ var data = [];
+
+ // Map of y value to index for faster look-up in the lineColorsCallback to
+ // get the status for each point.
+ var indexMap = {};
+ for (var i = 0; i < series.length; ++i) {
+ if (series[i].startTime == null || series[i].endTime == null) {
+ console.log("Each element in series must have startTime and endTime");
+ return;
+ }
+ var startTime = series[i].startTime;
+ var endTime = series[i].endTime;
+ if (startTime == -1 && endTime == -1) {
+ console.log("Ignoring data point with both start and end time invalid.");
+ continue;
+ }
+
+ var duration = 0;
+ if (endTime != -1 && startTime != -1) {
+ duration = endTime - startTime;
+ }
+ if (endTime == -1) {
+ endTime = new Date().getTime();
+ }
+ data.push({
+ time: endTime,
+ duration: duration
+ });
+
+ indexMap[endTime.toString()] = i;
+ }
+
+ if (data.length == 0) {
+ $(this.graphContainer).hide();
+ return;
+ }
+
+ var graphDiv = document.createElement('div');
+ $(this.graphContainer).html(graphDiv);
+
+ var lineColorsCallback = function(row, sidx, type) {
+ if (type != 'point') {
+ return "#000000";
+ }
+ var i = indexMap[row.x.toString()];
+ var status = series[i].status;
+ if (status == 'SKIPPED') {
+ return '#aaa';
+ }
+ else if (status == 'SUCCEEDED') {
+ return '#4e911e';
+ }
+ else if (status == 'RUNNING') {
+ return '#009fc9';
+ }
+ else if (status == 'PAUSED') {
+ return '#c92123';
+ }
+ else if (status == 'FAILED' ||
+ status == 'FAILED_FINISHING' ||
+ status == 'KILLED') {
+ return '#cc0000';
+ }
+ else {
+ return '#ccc';
+ }
+ };
+
+ var yLabelFormatCallback = function(y) {
+ var seconds = y / 1000.0;
+ return seconds.toString() + " s";
+ };
+
+ var hoverCallback = function(index, options, content) {
+ // Note: series contains the data points in descending order and index
+ // is the index into Morris's internal array of data sorted in ascending
+ // x order.
+ var status = series[options.data.length - index - 1].status;
+ return content +
+ '<div class="morris-hover-point">Status: ' + status + '</div>';
+ };
+
+ Morris.Line({
+ element: graphDiv,
+ data: data,
+ xkey: 'time',
+ ykeys: ['duration'],
+ labels: ['Duration'],
+ lineColors: lineColorsCallback,
+ yLabelFormat: yLabelFormatCallback,
+ hoverCallback: hoverCallback
+ });
+ }
+});
diff --git a/azkaban-webserver/src/web/js/azkaban/view/triggers.js b/azkaban-webserver/src/web/js/azkaban/view/triggers.js
new file mode 100644
index 0000000..a9c15a8
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/triggers.js
@@ -0,0 +1,349 @@
+/*
+ * 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.
+ */
+
+$.namespace('azkaban');
+
+function expireTrigger(triggerId) {
+ var triggerURL = contextURL + "/triggers"
+ var redirectURL = contextURL + "/triggers"
+ var requestData = {"ajax": "expireTrigger", "triggerId": triggerId};
+ var successHandler = function(data) {
+ if (data.error) {
+ //alert(data.error)
+ $('#errorMsg').text(data.error);
+ }
+ else {
+ //alert("Schedule "+schedId+" removed!")
+ window.location = redirectURL;
+ }
+ };
+ $.post(triggerURL, requestData, successHandler, "json");
+}
+
+function removeSched(scheduleId) {
+ var scheduleURL = contextURL + "/schedule"
+ var redirectURL = contextURL + "/schedule"
+ var requestData = {"action": "removeSched", "scheduleId": scheduleId};
+ var successHandler = function(data) {
+ if (data.error) {
+ //alert(data.error)
+ $('#errorMsg').text(data.error);
+ }
+ else {
+ //alert("Schedule "+schedId+" removed!")
+ window.location = redirectURL;
+ }
+ };
+ $.post(scheduleURL, requestData, successHandler, "json");
+}
+
+function removeSla(scheduleId) {
+ var scheduleURL = contextURL + "/schedule"
+ var redirectURL = contextURL + "/schedule"
+ var requestData = {"action": "removeSla", "scheduleId": scheduleId};
+ var successHandler = function(data) {
+ if (data.error) {
+ //alert(data.error)
+ $('#errorMsg').text(data.error)
+ }
+ else {
+ //alert("Schedule "+schedId+" removed!")
+ window.location = redirectURL
+ }
+ };
+ $.post(scheduleURL, requestData, successHandler, "json");
+}
+
+azkaban.ChangeSlaView = Backbone.View.extend({
+ events: {
+ "click" : "closeEditingTarget",
+ "click #set-sla-btn": "handleSetSla",
+ "click #remove-sla-btn": "handleRemoveSla",
+ "click #sla-cancel-btn": "handleSlaCancel",
+ "click .modal-close": "handleSlaCancel",
+ "click #addRow": "handleAddRow"
+ },
+
+ initialize: function(setting) {
+ },
+
+ handleSlaCancel: function(evt) {
+ console.log("Clicked cancel button");
+ var scheduleURL = contextURL + "/schedule";
+
+ $('#slaModalBackground').hide();
+ $('#sla-options').hide();
+
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ var rows = tFlowRules.rows;
+ var rowLength = rows.length
+ for (var i = 0; i < rowLength-1; i++) {
+ tFlowRules.deleteRow(0);
+ }
+ },
+
+ initFromSched: function(scheduleId, flowName) {
+ this.scheduleId = scheduleId;
+
+ var scheduleURL = contextURL + "/schedule"
+ this.scheduleURL = scheduleURL;
+ var indexToName = {};
+ var nameToIndex = {};
+ var indexToText = {};
+ this.indexToName = indexToName;
+ this.nameToIndex = nameToIndex;
+ this.indexToText = indexToText;
+ var ruleBoxOptions = ["SUCCESS", "FINISH"];
+ this.ruleBoxOptions = ruleBoxOptions;
+
+ var fetchScheduleData = {"scheduleId": this.scheduleId, "ajax": "slaInfo"};
+ var successHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ return;
+ }
+ if (data.slaEmails) {
+ $('#slaEmails').val(data.slaEmails.join());
+ }
+
+ var allJobNames = data.allJobNames;
+
+ indexToName[0] = "";
+ nameToIndex[flowName] = 0;
+ indexToText[0] = "flow " + flowName;
+ for (var i = 1; i <= allJobNames.length; i++) {
+ indexToName[i] = allJobNames[i-1];
+ nameToIndex[allJobNames[i-1]] = i;
+ indexToText[i] = "job " + allJobNames[i-1];
+ }
+
+ // populate with existing settings
+ if (data.settings) {
+ $('.durationpick').timepicker({hourMax: 99});
+ return;
+ }
+
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ for (var setting in data.settings) {
+ var rFlowRule = tFlowRules.insertRow(0);
+
+ var cId = rFlowRule.insertCell(-1);
+ var idSelect = document.createElement("select");
+ for (var i in indexToName) {
+ idSelect.options[i] = new Option(indexToText[i], indexToName[i]);
+ if (data.settings[setting].id == indexToName[i]) {
+ idSelect.options[i].selected = true;
+ }
+ }
+ cId.appendChild(idSelect);
+
+ var cRule = rFlowRule.insertCell(-1);
+ var ruleSelect = document.createElement("select");
+ for (var i in ruleBoxOptions) {
+ ruleSelect.options[i] = new Option(ruleBoxOptions[i], ruleBoxOptions[i]);
+ if (data.settings[setting].rule == ruleBoxOptions[i]) {
+ ruleSelect.options[i].selected = true;
+ }
+ }
+ cRule.appendChild(ruleSelect);
+
+ var cDuration = rFlowRule.insertCell(-1);
+ var duration = document.createElement("input");
+ duration.type = "text";
+ duration.setAttribute("class", "durationpick");
+ var rawMinutes = data.settings[setting].duration;
+ var intMinutes = rawMinutes.substring(0, rawMinutes.length-1);
+ var minutes = parseInt(intMinutes);
+ var hours = Math.floor(minutes / 60);
+ minutes = minutes % 60;
+ duration.value = hours + ":" + minutes;
+ cDuration.appendChild(duration);
+
+ var cEmail = rFlowRule.insertCell(-1);
+ var emailCheck = document.createElement("input");
+ emailCheck.type = "checkbox";
+ for (var act in data.settings[setting].actions) {
+ if (data.settings[setting].actions[act] == "EMAIL") {
+ emailCheck.checked = true;
+ }
+ }
+ cEmail.appendChild(emailCheck);
+
+ var cKill = rFlowRule.insertCell(-1);
+ var killCheck = document.createElement("input");
+ killCheck.type = "checkbox";
+ for (var act in data.settings[setting].actions) {
+ if (data.settings[setting].actions[act] == "KILL") {
+ killCheck.checked = true;
+ }
+ }
+ cKill.appendChild(killCheck);
+ $('.durationpick').timepicker({hourMax: 99});
+ }
+ $('.durationpick').timepicker({hourMax: 99});
+ };
+
+ $.get(this.scheduleURL, fetchScheduleData, successHandler, "json");
+ $('#slaModalBackground').show();
+ $('#sla-options').show();
+
+ //this.schedFlowOptions = sched.flowOptions
+ console.log("Loaded schedule info. Ready to set SLA.");
+ },
+
+ handleRemoveSla: function(evt) {
+ console.log("Clicked remove sla button");
+ var scheduleURL = this.scheduleURL;
+ var redirectURL = this.scheduleURL;
+ var requestData = {"action": "removeSla", "scheduleId": this.scheduleId};
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#errorMsg').text(data.error)
+ }
+ else {
+ window.location = redirectURL
+ }
+ };
+ $.post(scheduleURL, requestData, successHandler, "json");
+ },
+
+ handleSetSla: function(evt) {
+ var slaEmails = $('#slaEmails').val();
+ var settings = {};
+
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ for (var row = 0; row < tFlowRules.rows.length - 1; row++) {
+ var rFlowRule = tFlowRules.rows[row];
+ var id = rFlowRule.cells[0].firstChild.value;
+ var rule = rFlowRule.cells[1].firstChild.value;
+ var duration = rFlowRule.cells[2].firstChild.value;
+ var email = rFlowRule.cells[3].firstChild.checked;
+ var kill = rFlowRule.cells[4].firstChild.checked;
+ settings[row] = id + "," + rule + "," + duration + "," + email + "," + kill;
+ }
+
+ var slaData = {
+ scheduleId: this.scheduleId,
+ ajax: "setSla",
+ slaEmails: slaEmails,
+ settings: settings
+ };
+
+ var scheduleURL = this.scheduleURL;
+ var successHandler = function(data) {
+ if (data.error) {
+ alert(data.error);
+ }
+ else {
+ tFlowRules.length = 0;
+ window.location = scheduleURL;
+ }
+ };
+
+ $.post(scheduleURL, slaData, successHandler, "json");
+ },
+
+ handleAddRow: function(evt) {
+ var indexToName = this.indexToName;
+ var nameToIndex = this.nameToIndex;
+ var indexToText = this.indexToText;
+ var ruleBoxOptions = this.ruleBoxOptions;
+
+ var tFlowRules = document.getElementById("flowRulesTbl").tBodies[0];
+ var rFlowRule = tFlowRules.insertRow(tFlowRules.rows.length-1);
+
+ var cId = rFlowRule.insertCell(-1);
+ var idSelect = document.createElement("select");
+ for (var i in indexToName) {
+ idSelect.options[i] = new Option(indexToText[i], indexToName[i]);
+ }
+
+ cId.appendChild(idSelect);
+
+ var cRule = rFlowRule.insertCell(-1);
+ var ruleSelect = document.createElement("select");
+ for (var i in ruleBoxOptions) {
+ ruleSelect.options[i] = new Option(ruleBoxOptions[i], ruleBoxOptions[i]);
+ }
+ cRule.appendChild(ruleSelect);
+
+ var cDuration = rFlowRule.insertCell(-1);
+ var duration = document.createElement("input");
+ duration.type = "text";
+ duration.setAttribute("class", "durationpick");
+ cDuration.appendChild(duration);
+
+ var cEmail = rFlowRule.insertCell(-1);
+ var emailCheck = document.createElement("input");
+ emailCheck.type = "checkbox";
+ cEmail.appendChild(emailCheck);
+
+ var cKill = rFlowRule.insertCell(-1);
+ var killCheck = document.createElement("input");
+ killCheck.type = "checkbox";
+ cKill.appendChild(killCheck);
+
+ $('.durationpick').timepicker({hourMax: 99});
+ return rFlowRule;
+ },
+
+ handleEditColumn: function(evt) {
+ var curTarget = evt.currentTarget;
+
+ if (this.editingTarget != curTarget) {
+ this.closeEditingTarget();
+
+ var text = $(curTarget).children(".spanValue").text();
+ $(curTarget).empty();
+
+ var input = document.createElement("input");
+ $(input).attr("type", "text");
+ $(input).css("width", "100%");
+ $(input).val(text);
+ $(curTarget).addClass("editing");
+ $(curTarget).append(input);
+ $(input).focus();
+ this.editingTarget = curTarget;
+ }
+ },
+
+ handleRemoveColumn: function(evt) {
+ var curTarget = evt.currentTarget;
+ // Should be the table
+ var row = curTarget.parentElement.parentElement;
+ $(row).remove();
+ },
+
+ closeEditingTarget: function(evt) {
+ }
+});
+
+var slaView;
+var tableSorterView;
+$(function() {
+ var selected;
+
+ slaView = new azkaban.ChangeSlaView({el:$('#sla-options')});
+ tableSorterView = new azkaban.TableSorter({el:$('#scheduledFlowsTbl')});
+ /*
+ var requestURL = contextURL + "/manager";
+
+ // Set up the Flow options view. Create a new one every time :p
+ $('#addSlaBtn').click( function() {
+ slaView.show();
+ });
+ */
+});
diff --git a/azkaban-webserver/src/web/js/jquery.svganim.min.js b/azkaban-webserver/src/web/js/jquery.svganim.min.js
new file mode 100644
index 0000000..3cc4020
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery.svganim.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+ SVG attribute animations for jQuery v1.4.5.
+ Written by Keith Wood (kbwood{at}iinet.com.au) June 2008.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+(function($){$.each(['x','y','width','height','rx','ry','cx','cy','r','x1','y1','x2','y2','stroke-width','strokeWidth','opacity','fill-opacity','fillOpacity','stroke-opacity','strokeOpacity','stroke-dashoffset','strokeDashOffset','font-size','fontSize','font-weight','fontWeight','letter-spacing','letterSpacing','word-spacing','wordSpacing'],function(i,f){var g=f.charAt(0).toUpperCase()+f.substr(1);if($.cssProps){$.cssProps['svg'+g]=$.cssProps['svg-'+f]=f}$.fx.step['svg'+g]=$.fx.step['svg-'+f]=function(a){var b=$.svg._attrNames[f]||f;var c=a.elem.attributes.getNamedItem(b);if(!a.set){a.start=(c?parseFloat(c.nodeValue):0);var d=($.fn.jquery>='1.6'?'':a.options.curAnim['svg'+g]||a.options.curAnim['svg-'+f]);if(/^[+-]=/.exec(d)){a.end=a.start+parseFloat(d.replace(/=/,''))}$(a.elem).css(b,'');a.set=true}var e=(a.pos*(a.end-a.start)+a.start)+(a.unit=='%'?'%':'');(c?c.nodeValue=e:a.elem.setAttribute(b,e))}});$.fx.step['svgStrokeDashArray']=$.fx.step['svg-strokeDashArray']=$.fx.step['svgStroke-dasharray']=$.fx.step['svg-stroke-dasharray']=function(a){var b=a.elem.attributes.getNamedItem('stroke-dasharray');if(!a.set){a.start=parseDashArray(b?b.nodeValue:'');var c=($.fn.jquery>='1.6'?a.end:a.options.curAnim['svgStrokeDashArray']||a.options.curAnim['svg-strokeDashArray']||a.options.curAnim['svgStroke-dasharray']||a.options.curAnim['svg-stroke-dasharray']);a.end=parseDashArray(c);if(/^[+-]=/.exec(c)){c=c.split(/[, ]+/);if(c.length%2==1){var d=c.length;for(var i=0;i<d;i++){c.push(c[i])}}for(var i=0;i<c.length;i++){if(/^[+-]=/.exec(c[i])){a.end[i]=a.start[i]+parseFloat(c[i].replace(/=/,''))}}}a.set=true}var e=$.map(a.start,function(n,i){return(a.pos*(a.end[i]-n)+n)}).join(',');(b?b.nodeValue=e:a.elem.setAttribute('stroke-dasharray',e))};function parseDashArray(a){var b=a.split(/[, ]+/);for(var i=0;i<b.length;i++){b[i]=parseFloat(b[i]);if(isNaN(b[i])){b[i]=0}}if(b.length%2==1){var c=b.length;for(var i=0;i<c;i++){b.push(b[i])}}return b}$.fx.step['svgViewBox']=$.fx.step['svg-viewBox']=function(a){var b=a.elem.attributes.getNamedItem('viewBox');if(!a.set){a.start=parseViewBox(b?b.nodeValue:'');var c=($.fn.jquery>='1.6'?a.end:a.options.curAnim['svgViewBox']||a.options.curAnim['svg-viewBox']);a.end=parseViewBox(c);if(/^[+-]=/.exec(c)){c=c.split(/[, ]+/);while(c.length<4){c.push('0')}for(var i=0;i<4;i++){if(/^[+-]=/.exec(c[i])){a.end[i]=a.start[i]+parseFloat(c[i].replace(/=/,''))}}}a.set=true}var d=$.map(a.start,function(n,i){return(a.pos*(a.end[i]-n)+n)}).join(' ');(b?b.nodeValue=d:a.elem.setAttribute('viewBox',d))};function parseViewBox(a){var b=a.split(/[, ]+/);for(var i=0;i<b.length;i++){b[i]=parseFloat(b[i]);if(isNaN(b[i])){b[i]=0}}while(b.length<4){b.push(0)}return b}$.fx.step['svgTransform']=$.fx.step['svg-transform']=function(a){var b=a.elem.attributes.getNamedItem('transform');if(!a.set){a.start=parseTransform(b?b.nodeValue:'');a.end=parseTransform(a.end,a.start);a.set=true}var c='';for(var i=0;i<a.end.order.length;i++){switch(a.end.order.charAt(i)){case't':c+=' translate('+(a.pos*(a.end.translateX-a.start.translateX)+a.start.translateX)+','+(a.pos*(a.end.translateY-a.start.translateY)+a.start.translateY)+')';break;case's':c+=' scale('+(a.pos*(a.end.scaleX-a.start.scaleX)+a.start.scaleX)+','+(a.pos*(a.end.scaleY-a.start.scaleY)+a.start.scaleY)+')';break;case'r':c+=' rotate('+(a.pos*(a.end.rotateA-a.start.rotateA)+a.start.rotateA)+','+(a.pos*(a.end.rotateX-a.start.rotateX)+a.start.rotateX)+','+(a.pos*(a.end.rotateY-a.start.rotateY)+a.start.rotateY)+')';break;case'x':c+=' skewX('+(a.pos*(a.end.skewX-a.start.skewX)+a.start.skewX)+')';case'y':c+=' skewY('+(a.pos*(a.end.skewY-a.start.skewY)+a.start.skewY)+')';break;case'm':var d='';for(var j=0;j<6;j++){d+=','+(a.pos*(a.end.matrix[j]-a.start.matrix[j])+a.start.matrix[j])}c+=' matrix('+d.substr(1)+')';break}}(b?b.nodeValue=c:a.elem.setAttribute('transform',c))};function parseTransform(a,b){a=a||'';if(typeof a=='object'){a=a.nodeValue}var c=$.extend({translateX:0,translateY:0,scaleX:0,scaleY:0,rotateA:0,rotateX:0,rotateY:0,skewX:0,skewY:0,matrix:[0,0,0,0,0,0]},b||{});c.order='';var d=/([a-zA-Z]+)\(\s*([+-]?[\d\.]+)\s*(?:[\s,]\s*([+-]?[\d\.]+)\s*(?:[\s,]\s*([+-]?[\d\.]+)\s*(?:[\s,]\s*([+-]?[\d\.]+)\s*[\s,]\s*([+-]?[\d\.]+)\s*[\s,]\s*([+-]?[\d\.]+)\s*)?)?)?\)/g;var e=d.exec(a);while(e){switch(e[1]){case'translate':c.order+='t';c.translateX=parseFloat(e[2]);c.translateY=(e[3]?parseFloat(e[3]):0);break;case'scale':c.order+='s';c.scaleX=parseFloat(e[2]);c.scaleY=(e[3]?parseFloat(e[3]):c.scaleX);break;case'rotate':c.order+='r';c.rotateA=parseFloat(e[2]);c.rotateX=(e[3]?parseFloat(e[3]):0);c.rotateY=(e[4]?parseFloat(e[4]):0);break;case'skewX':c.order+='x';c.skewX=parseFloat(e[2]);break;case'skewY':c.order+='y';c.skewY=parseFloat(e[2]);break;case'matrix':c.order+='m';c.matrix=[parseFloat(e[2]),parseFloat(e[3]),parseFloat(e[4]),parseFloat(e[5]),parseFloat(e[6]),parseFloat(e[7])];break}e=d.exec(a)}if(c.order=='m'&&Math.abs(c.matrix[0])==Math.abs(c.matrix[3])&&c.matrix[1]!=0&&Math.abs(c.matrix[1])==Math.abs(c.matrix[2])){var f=Math.acos(c.matrix[0])*180/Math.PI;f=(c.matrix[1]<0?360-f:f);c.order='rt';c.rotateA=f;c.rotateX=c.rotateY=0;c.translateX=c.matrix[4];c.translateY=c.matrix[5]}return c}$.each(['fill','stroke'],function(i,e){var f=e.charAt(0).toUpperCase()+e.substr(1);$.fx.step['svg'+f]=$.fx.step['svg-'+e]=function(a){if(!a.set){a.start=$.svg._getColour(a.elem,e);var b=(a.end=='none');a.end=(b?$.svg._getColour(a.elem.parentNode,e):$.svg._getRGB(a.end));a.end[3]=b;$(a.elem).css(e,'');a.set=true}var c=a.elem.attributes.getNamedItem(e);var d='rgb('+[Math.min(Math.max(parseInt((a.pos*(a.end[0]-a.start[0]))+a.start[0],10),0),255),Math.min(Math.max(parseInt((a.pos*(a.end[1]-a.start[1]))+a.start[1],10),0),255),Math.min(Math.max(parseInt((a.pos*(a.end[2]-a.start[2]))+a.start[2],10),0),255)].join(',')+')';d=(a.end[3]&&a.state==1?'none':d);(c?c.nodeValue=d:a.elem.setAttribute(e,d))}});$.svg._getColour=function(a,b){a=$(a);var c;do{c=a.attr(b)||a.css(b);if((c!=''&&c!='none')||a.hasClass($.svg.markerClassName)){break}}while(a=a.parent());return $.svg._getRGB(c)};$.svg._getRGB=function(a){var b;if(a&&a.constructor==Array){return(a.length==3||a.length==4?a:h['none'])}if(b=/^rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)$/.exec(a)){return[parseInt(b[1],10),parseInt(b[2],10),parseInt(b[3],10)]}if(b=/^rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)$/.exec(a)){return[parseFloat(b[1])*2.55,parseFloat(b[2])*2.55,parseFloat(b[3])*2.55]}if(b=/^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/.exec(a)){return[parseInt(b[1],16),parseInt(b[2],16),parseInt(b[3],16)]}if(b=/^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/.exec(a)){return[parseInt(b[1]+b[1],16),parseInt(b[2]+b[2],16),parseInt(b[3]+b[3],16)]}return h[$.trim(a).toLowerCase()]||h['none']};var h={'':[255,255,255,1],none:[255,255,255,1],aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],grey:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}})(jQuery);
\ No newline at end of file
azkaban-webserver/src/web/js/jquery.svgdom.js 406(+406 -0)
diff --git a/azkaban-webserver/src/web/js/jquery.svgdom.js b/azkaban-webserver/src/web/js/jquery.svgdom.js
new file mode 100644
index 0000000..b5ad8af
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery.svgdom.js
@@ -0,0 +1,406 @@
+/* http://keith-wood.name/svg.html
+ jQuery DOM compatibility for jQuery SVG v1.4.5.
+ Written by Keith Wood (kbwood{at}iinet.com.au) April 2009.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+
+(function($) { // Hide scope, no $ conflict
+
+/* Support adding class names to SVG nodes. */
+$.fn.addClass = function(origAddClass) {
+ return function(classNames) {
+ classNames = classNames || '';
+ return this.each(function() {
+ if ($.svg.isSVGElem(this)) {
+ var node = this;
+ $.each(classNames.split(/\s+/), function(i, className) {
+ var classes = (node.className ? node.className.baseVal : node.getAttribute('class'));
+ if ($.inArray(className, classes.split(/\s+/)) == -1) {
+ classes += (classes ? ' ' : '') + className;
+ (node.className ? node.className.baseVal = classes :
+ node.setAttribute('class', classes));
+ }
+ });
+ }
+ else {
+ origAddClass.apply($(this), [classNames]);
+ }
+ });
+ };
+}($.fn.addClass);
+
+/* Support removing class names from SVG nodes. */
+$.fn.removeClass = function(origRemoveClass) {
+ return function(classNames) {
+ classNames = classNames || '';
+ return this.each(function() {
+ if ($.svg.isSVGElem(this)) {
+ var node = this;
+ $.each(classNames.split(/\s+/), function(i, className) {
+ var classes = (node.className ? node.className.baseVal : node.getAttribute('class'));
+ classes = $.grep(classes.split(/\s+/), function(n, i) { return n != className; }).
+ join(' ');
+ (node.className ? node.className.baseVal = classes :
+ node.setAttribute('class', classes));
+ });
+ }
+ else {
+ origRemoveClass.apply($(this), [classNames]);
+ }
+ });
+ };
+}($.fn.removeClass);
+
+/* Support toggling class names on SVG nodes. */
+$.fn.toggleClass = function(origToggleClass) {
+ return function(className, state) {
+ return this.each(function() {
+ if ($.svg.isSVGElem(this)) {
+ if (typeof state !== 'boolean') {
+ state = !$(this).hasClass(className);
+ }
+ $(this)[(state ? 'add' : 'remove') + 'Class'](className);
+ }
+ else {
+ origToggleClass.apply($(this), [className, state]);
+ }
+ });
+ };
+}($.fn.toggleClass);
+
+/* Support checking class names on SVG nodes. */
+$.fn.hasClass = function(origHasClass) {
+ return function(className) {
+ className = className || '';
+ var found = false;
+ this.each(function() {
+ if ($.svg.isSVGElem(this)) {
+ var classes = (this.className ? this.className.baseVal :
+ this.getAttribute('class')).split(/\s+/);
+ found = ($.inArray(className, classes) > -1);
+ }
+ else {
+ found = (origHasClass.apply($(this), [className]));
+ }
+ return !found;
+ });
+ return found;
+ };
+}($.fn.hasClass);
+
+/* Support attributes on SVG nodes. */
+$.fn.attr = function(origAttr) {
+ return function(name, value, type) {
+ if (typeof name === 'string' && value === undefined) {
+ var val = origAttr.apply(this, [name]);
+ if (val && val.baseVal && val.baseVal.numberOfItems != null) { // Multiple values
+ value = '';
+ val = val.baseVal;
+ if (name == 'transform') {
+ for (var i = 0; i < val.numberOfItems; i++) {
+ var item = val.getItem(i);
+ switch (item.type) {
+ case 1: value += ' matrix(' + item.matrix.a + ',' + item.matrix.b + ',' +
+ item.matrix.c + ',' + item.matrix.d + ',' +
+ item.matrix.e + ',' + item.matrix.f + ')';
+ break;
+ case 2: value += ' translate(' + item.matrix.e + ',' + item.matrix.f + ')'; break;
+ case 3: value += ' scale(' + item.matrix.a + ',' + item.matrix.d + ')'; break;
+ case 4: value += ' rotate(' + item.angle + ')'; break; // Doesn't handle new origin
+ case 5: value += ' skewX(' + item.angle + ')'; break;
+ case 6: value += ' skewY(' + item.angle + ')'; break;
+ }
+ }
+ val = value.substring(1);
+ }
+ else {
+ val = val.getItem(0).valueAsString;
+ }
+ }
+ return (val && val.baseVal ? val.baseVal.valueAsString : val);
+ }
+
+ var options = name;
+ if (typeof name === 'string') {
+ options = {};
+ options[name] = value;
+ }
+ return this.each(function() {
+ if ($.svg.isSVGElem(this)) {
+ for (var n in options) {
+ var val = ($.isFunction(options[n]) ? options[n]() : options[n]);
+ (type ? this.style[n] = val : this.setAttribute(n, val));
+ }
+ }
+ else {
+ origAttr.apply($(this), [name, value, type]);
+ }
+ });
+ };
+}($.fn.attr);
+
+/* Support removing attributes on SVG nodes. */
+$.fn.removeAttr = function(origRemoveAttr) {
+ return function(name) {
+ return this.each(function() {
+ if ($.svg.isSVGElem(this)) {
+ (this[name] && this[name].baseVal ? this[name].baseVal.value = '' :
+ this.setAttribute(name, ''));
+ }
+ else {
+ origRemoveAttr.apply($(this), [name]);
+ }
+ });
+ };
+}($.fn.removeAttr);
+
+/* Add numeric only properties. */
+$.extend($.cssNumber, {
+ 'stopOpacity': true,
+ 'strokeMitrelimit': true,
+ 'strokeOpacity': true
+});
+
+/* Support retrieving CSS/attribute values on SVG nodes. */
+if ($.cssProps) {
+ $.css = function(origCSS) {
+ return function(elem, name, extra) {
+ var value = (name.match(/^svg.*/) ? $(elem).attr($.cssProps[name] || name) : '');
+ return value || origCSS(elem, name, extra);
+ };
+ }($.css);
+}
+
+/* Determine if any nodes are SVG nodes. */
+function anySVG(checkSet) {
+ for (var i = 0; i < checkSet.length; i++) {
+ if (checkSet[i].nodeType == 1 && checkSet[i].namespaceURI == $.svg.svgNS) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* Update Sizzle selectors. */
+
+$.expr.relative['+'] = function(origRelativeNext) {
+ return function(checkSet, part, isXML) {
+ origRelativeNext(checkSet, part, isXML || anySVG(checkSet));
+ };
+}($.expr.relative['+']);
+
+$.expr.relative['>'] = function(origRelativeChild) {
+ return function(checkSet, part, isXML) {
+ origRelativeChild(checkSet, part, isXML || anySVG(checkSet));
+ };
+}($.expr.relative['>']);
+
+$.expr.relative[''] = function(origRelativeDescendant) {
+ return function(checkSet, part, isXML) {
+ origRelativeDescendant(checkSet, part, isXML || anySVG(checkSet));
+ };
+}($.expr.relative['']);
+
+$.expr.relative['~'] = function(origRelativeSiblings) {
+ return function(checkSet, part, isXML) {
+ origRelativeSiblings(checkSet, part, isXML || anySVG(checkSet));
+ };
+}($.expr.relative['~']);
+
+$.expr.find.ID = function(origFindId) {
+ return function(match, context, isXML) {
+ return ($.svg.isSVGElem(context) ?
+ [context.ownerDocument.getElementById(match[1])] :
+ origFindId(match, context, isXML));
+ };
+}($.expr.find.ID);
+
+var div = document.createElement('div');
+div.appendChild(document.createComment(''));
+if (div.getElementsByTagName('*').length > 0) { // Make sure no comments are found
+ $.expr.find.TAG = function(match, context) {
+ var results = context.getElementsByTagName(match[1]);
+ if (match[1] === '*') { // Filter out possible comments
+ var tmp = [];
+ for (var i = 0; results[i] || results.item(i); i++) {
+ if ((results[i] || results.item(i)).nodeType === 1) {
+ tmp.push(results[i] || results.item(i));
+ }
+ }
+ results = tmp;
+ }
+ return results;
+ };
+}
+
+$.expr.preFilter.CLASS = function(match, curLoop, inplace, result, not, isXML) {
+ match = ' ' + match[1].replace(/\\/g, '') + ' ';
+ if (isXML) {
+ return match;
+ }
+ for (var i = 0, elem = {}; elem != null; i++) {
+ elem = curLoop[i];
+ if (!elem) {
+ try {
+ elem = curLoop.item(i);
+ }
+ catch (e) {
+ // Ignore
+ }
+ }
+ if (elem) {
+ var className = (!$.svg.isSVGElem(elem) ? elem.className :
+ (elem.className ? elem.className.baseVal : '') || elem.getAttribute('class'));
+ if (not ^ (className && (' ' + className + ' ').indexOf(match) > -1)) {
+ if (!inplace)
+ result.push(elem);
+ }
+ else if (inplace) {
+ curLoop[i] = false;
+ }
+ }
+ }
+ return false;
+};
+
+$.expr.filter.CLASS = function(elem, match) {
+ var className = (!$.svg.isSVGElem(elem) ? elem.className :
+ (elem.className ? elem.className.baseVal : elem.getAttribute('class')));
+ return (' ' + className + ' ').indexOf(match) > -1;
+};
+
+$.expr.filter.ATTR = function(origFilterAttr) {
+ return function(elem, match) {
+ var handler = null;
+ if ($.svg.isSVGElem(elem)) {
+ handler = match[1];
+ $.expr.attrHandle[handler] = function(elem){
+ var attr = elem.getAttribute(handler);
+ return attr && attr.baseVal || attr;
+ };
+ }
+ var filter = origFilterAttr(elem, match);
+ if (handler) {
+ $.expr.attrHandle[handler] = null;
+ }
+ return filter;
+ };
+}($.expr.filter.ATTR);
+
+/*
+ In the removeData function (line 1881, v1.7.2):
+
+ if ( jQuery.support.deleteExpando ) {
+ delete elem[ internalKey ];
+ } else {
+ try { // SVG
+ elem.removeAttribute( internalKey );
+ } catch (e) {
+ elem[ internalKey ] = null;
+ }
+ }
+
+ In the event.add function (line 2985, v1.7.2):
+
+ if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+ // Bind the global event handler to the element
+ try { // SVG
+ elem.addEventListener( type, eventHandle, false );
+ } catch(e) {
+ if ( elem.attachEvent ) {
+ elem.attachEvent( "on" + type, eventHandle );
+ }
+ }
+ }
+
+ In the event.remove function (line 3074, v1.7.2):
+
+ if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
+ try { // SVG
+ elem.removeEventListener(type, elemData.handle, false);
+ }
+ catch (e) {
+ if (elem.detachEvent) {
+ elem.detachEvent("on" + type, elemData.handle);
+ }
+ }
+ }
+
+ In the event.fix function (line 3394, v1.7.2):
+
+ if (event.target.namespaceURI == 'http://www.w3.org/2000/svg') { // SVG
+ event.button = [1, 4, 2][event.button];
+ }
+
+ // Add which for click: 1 === left; 2 === middle; 3 === right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && button !== undefined ) {
+ event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
+ }
+
+ In the Sizzle function (line 4083, v1.7.2):
+
+ if ( toString.call(checkSet) === "[object Array]" ) {
+ if ( !prune ) {
+ results.push.apply( results, checkSet );
+
+ } else if ( context && context.nodeType === 1 ) {
+ for ( i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
+ results.push( set[i] || set.item(i) ); // SVG
+ }
+ }
+
+ } else {
+ for ( i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+ results.push( set[i] || set.item(i) ); // SVG
+ }
+ }
+ }
+ } else {...
+
+ In the fallback for the Sizzle makeArray function (line 4877, v1.7.2):
+
+ if ( toString.call(array) === "[object Array]" ) {
+ Array.prototype.push.apply( ret, array );
+
+ } else {
+ if ( typeof array.length === "number" ) {
+ for ( var l = array.length; i < l; i++ ) {
+ ret.push( array[i] || array.item(i) ); // SVG
+ }
+
+ } else {
+ for ( ; array[i]; i++ ) {
+ ret.push( array[i] );
+ }
+ }
+ }
+
+ In the jQuery.cleandata function (line 6538, v1.7.2):
+
+ if ( deleteExpando ) {
+ delete elem[ jQuery.expando ];
+
+ } else {
+ try { // SVG
+ elem.removeAttribute( jQuery.expando );
+ } catch (e) {
+ // Ignore
+ }
+ }
+
+ In the fallback getComputedStyle function (line 6727, v1.7.2):
+
+ defaultView = (elem.ownerDocument ? elem.ownerDocument.defaultView : elem.defaultView); // SVG
+ if ( defaultView &&
+ (computedStyle = defaultView.getComputedStyle( elem, null )) ) {
+
+ ret = computedStyle.getPropertyValue( name );
+ ...
+
+*/
+
+})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jquery.svgdom.min.js b/azkaban-webserver/src/web/js/jquery.svgdom.min.js
new file mode 100644
index 0000000..3c280a5
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery.svgdom.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+ jQuery DOM compatibility for jQuery SVG v1.4.5.
+ Written by Keith Wood (kbwood{at}iinet.com.au) April 2009.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+(function($){$.fn.addClass=function(e){return function(d){d=d||'';return this.each(function(){if($.svg.isSVGElem(this)){var c=this;$.each(d.split(/\s+/),function(i,a){var b=(c.className?c.className.baseVal:c.getAttribute('class'));if($.inArray(a,b.split(/\s+/))==-1){b+=(b?' ':'')+a;(c.className?c.className.baseVal=b:c.setAttribute('class',b))}})}else{e.apply($(this),[d])}})}}($.fn.addClass);$.fn.removeClass=function(e){return function(d){d=d||'';return this.each(function(){if($.svg.isSVGElem(this)){var c=this;$.each(d.split(/\s+/),function(i,a){var b=(c.className?c.className.baseVal:c.getAttribute('class'));b=$.grep(b.split(/\s+/),function(n,i){return n!=a}).join(' ');(c.className?c.className.baseVal=b:c.setAttribute('class',b))})}else{e.apply($(this),[d])}})}}($.fn.removeClass);$.fn.toggleClass=function(c){return function(a,b){return this.each(function(){if($.svg.isSVGElem(this)){if(typeof b!=='boolean'){b=!$(this).hasClass(a)}$(this)[(b?'add':'remove')+'Class'](a)}else{c.apply($(this),[a,b])}})}}($.fn.toggleClass);$.fn.hasClass=function(d){return function(b){b=b||'';var c=false;this.each(function(){if($.svg.isSVGElem(this)){var a=(this.className?this.className.baseVal:this.getAttribute('class')).split(/\s+/);c=($.inArray(b,a)>-1)}else{c=(d.apply($(this),[b]))}return!c});return c}}($.fn.hasClass);$.fn.attr=function(h){return function(b,c,d){if(typeof b==='string'&&c===undefined){var e=h.apply(this,[b]);if(e&&e.baseVal&&e.baseVal.numberOfItems!=null){c='';e=e.baseVal;if(b=='transform'){for(var i=0;i<e.numberOfItems;i++){var f=e.getItem(i);switch(f.type){case 1:c+=' matrix('+f.matrix.a+','+f.matrix.b+','+f.matrix.c+','+f.matrix.d+','+f.matrix.e+','+f.matrix.f+')';break;case 2:c+=' translate('+f.matrix.e+','+f.matrix.f+')';break;case 3:c+=' scale('+f.matrix.a+','+f.matrix.d+')';break;case 4:c+=' rotate('+f.angle+')';break;case 5:c+=' skewX('+f.angle+')';break;case 6:c+=' skewY('+f.angle+')';break}}e=c.substring(1)}else{e=e.getItem(0).valueAsString}}return(e&&e.baseVal?e.baseVal.valueAsString:e)}var g=b;if(typeof b==='string'){g={};g[b]=c}return this.each(function(){if($.svg.isSVGElem(this)){for(var n in g){var a=($.isFunction(g[n])?g[n]():g[n]);(d?this.style[n]=a:this.setAttribute(n,a))}}else{h.apply($(this),[b,c,d])}})}}($.fn.attr);$.fn.removeAttr=function(b){return function(a){return this.each(function(){if($.svg.isSVGElem(this)){(this[a]&&this[a].baseVal?this[a].baseVal.value='':this.setAttribute(a,''))}else{b.apply($(this),[a])}})}}($.fn.removeAttr);$.extend($.cssNumber,{'stopOpacity':true,'strokeMitrelimit':true,'strokeOpacity':true});if($.cssProps){$.css=function(e){return function(a,b,c){var d=(b.match(/^svg.*/)?$(a).attr($.cssProps[b]||b):'');return d||e(a,b,c)}}($.css)}function anySVG(a){for(var i=0;i<a.length;i++){if(a[i].nodeType==1&&a[i].namespaceURI==$.svg.svgNS){return true}}return false}$.expr.relative['+']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['+']);$.expr.relative['>']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['>']);$.expr.relative['']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['']);$.expr.relative['~']=function(d){return function(a,b,c){d(a,b,c||anySVG(a))}}($.expr.relative['~']);$.expr.find.ID=function(d){return function(a,b,c){return($.svg.isSVGElem(b)?[b.ownerDocument.getElementById(a[1])]:d(a,b,c))}}($.expr.find.ID);var j=document.createElement('div');j.appendChild(document.createComment(''));if(j.getElementsByTagName('*').length>0){$.expr.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==='*'){var d=[];for(var i=0;c[i]||c.item(i);i++){if((c[i]||c.item(i)).nodeType===1){d.push(c[i]||c.item(i))}}c=d}return c}}$.expr.preFilter.CLASS=function(a,b,c,d,f,g){a=' '+a[1].replace(/\\/g,'')+' ';if(g){return a}for(var i=0,elem={};elem!=null;i++){elem=b[i];if(!elem){try{elem=b.item(i)}catch(e){}}if(elem){var h=(!$.svg.isSVGElem(elem)?elem.className:(elem.className?elem.className.baseVal:'')||elem.getAttribute('class'));if(f^(h&&(' '+h+' ').indexOf(a)>-1)){if(!c)d.push(elem)}else if(c){b[i]=false}}}return false};$.expr.filter.CLASS=function(a,b){var c=(!$.svg.isSVGElem(a)?a.className:(a.className?a.className.baseVal:a.getAttribute('class')));return(' '+c+' ').indexOf(b)>-1};$.expr.filter.ATTR=function(g){return function(c,d){var e=null;if($.svg.isSVGElem(c)){e=d[1];$.expr.attrHandle[e]=function(a){var b=a.getAttribute(e);return b&&b.baseVal||b}}var f=g(c,d);if(e){$.expr.attrHandle[e]=null}return f}}($.expr.filter.ATTR)})(jQuery);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery.svgfilter.min.js b/azkaban-webserver/src/web/js/jquery.svgfilter.min.js
new file mode 100644
index 0000000..551bdc9
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery.svgfilter.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+ SVG filters for jQuery v1.4.5.
+ Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+(function($){$.svg.addExtension('filters',SVGFilter);$.extend($.svg._wrapperClass.prototype,{filter:function(a,b,x,y,c,d,e){var f=this._args(arguments,['id','x','y','width','height']);return this._makeNode(f.parent,'filter',$.extend({id:f.id,x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}))}});function SVGFilter(a){this._wrapper=a}$.extend(SVGFilter.prototype,{distantLight:function(a,b,c,d,e){var f=this._wrapper._args(arguments,['result','azimuth','elevation']);return this._wrapper._makeNode(f.parent,'feDistantLight',$.extend({result:f.result,azimuth:f.azimuth,elevation:f.elevation},f.settings||{}))},pointLight:function(a,b,x,y,z,c){var d=this._wrapper._args(arguments,['result','x','y','z']);return this._wrapper._makeNode(d.parent,'fePointLight',$.extend({result:d.result,x:d.x,y:d.y,z:d.z},d.settings||{}))},spotLight:function(a,b,x,y,z,c,d,e,f){var g=this._wrapper._args(arguments,['result','x','y','z','toX','toY','toZ'],['toX']);var h=$.extend({result:g.result,x:g.x,y:g.y,z:g.z},(g.toX!=null?{pointsAtX:g.toX,pointsAtY:g.toY,pointsAtZ:g.toZ}:{}));return this._wrapper._makeNode(g.parent,'feSpotLight',$.extend(h,g.settings||{}))},blend:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','mode','in1','in2']);return this._wrapper._makeNode(g.parent,'feBlend',$.extend({result:g.result,mode:g.mode,in_:g.in1,in2:g.in2},g.settings||{}))},colorMatrix:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','in1','type','values']);if(isArray(g.values)){var h='';for(var i=0;i<g.values.length;i++){h+=(i==0?'':' ')+g.values[i].join(' ')}g.values=h}else if(typeof g.values=='object'){g.settings=g.values;g.values=null}var j=$.extend({result:g.result,in_:g.in1,type:g.type},(g.values!=null?{values:g.values}:{}));return this._wrapper._makeNode(g.parent,'feColorMatrix',$.extend(j,g.settings||{}))},componentTransfer:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','functions']);var f=this._wrapper._makeNode(e.parent,'feComponentTransfer',$.extend({result:e.result},e.settings||{}));var g=['R','G','B','A'];for(var i=0;i<Math.min(4,e.functions.length);i++){var h=e.functions[i];var j=$.extend({type:h[0]},(h[0]=='table'||h[0]=='discrete'?{tableValues:h[1].join(' ')}:(h[0]=='linear'?{slope:h[1],intercept:h[2]}:(h[0]=='gamma'?{amplitude:h[1],exponent:h[2],offset:h[3]}:{}))));this._wrapper._makeNode(f,'feFunc'+g[i],j)}return f},composite:function(a,b,c,d,e,f,g,h,i,j){var k=this._wrapper._args(arguments,['result','operator','in1','in2','k1','k2','k3','k4'],['k1']);var l=$.extend({result:k.result,operator:k.operator,'in':k.in1,in2:k.in2},(k.k1!=null?{k1:k.k1,k2:k.k2,k3:k.k3,k4:k.k4}:{}));return this._wrapper._makeNode(k.parent,'feComposite',$.extend(l,k.settings||{}))},convolveMatrix:function(a,b,c,d,e){var f=this._wrapper._args(arguments,['result','order','matrix']);var g='';for(var i=0;i<f.matrix.length;i++){g+=(i==0?'':' ')+f.matrix[i].join(' ')}f.matrix=g;return this._wrapper._makeNode(f.parent,'feConvolveMatrix',$.extend({result:f.result,order:f.order,kernelMatrix:f.matrix},f.settings||{}))},diffuseLighting:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','colour'],['colour']);return this._wrapper._makeNode(e.parent,'feDiffuseLighting',$.extend($.extend({result:e.result},(e.colour?{lightingColor:e.colour}:{})),e.settings||{}))},displacementMap:function(a,b,c,d,e){var f=this._wrapper._args(arguments,['result','in1','in2']);return this._wrapper._makeNode(f.parent,'feDisplacementMap',$.extend({result:f.result,in_:f.in1,in2:f.in2},f.settings||{}))},flood:function(a,b,x,y,c,d,e,f,g){var h=this._wrapper._args(arguments,['result','x','y','width','height','colour','opacity']);if(arguments.length<6){h.colour=h.x;h.opacity=h.y;h.settings=h.width;h.x=null}var i=$.extend({result:h.result,floodColor:h.colour,floodOpacity:h.opacity},(h.x!=null?{x:h.x,y:h.y,width:h.width,height:h.height}:{}));return this._wrapper._makeNode(h.parent,'feFlood',$.extend(i,h.settings||{}))},gaussianBlur:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','in1','stdDevX','stdDevY'],['stdDevY']);return this._wrapper._makeNode(g.parent,'feGaussianBlur',$.extend({result:g.result,in_:g.in1,stdDeviation:g.stdDevX+(g.stdDevY?' '+g.stdDevY:'')},g.settings||{}))},image:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','href']);var f=this._wrapper._makeNode(e.parent,'feImage',$.extend({result:e.result},e.settings||{}));f.setAttributeNS($.svg.xlinkNS,'href',e.href);return f},merge:function(a,b,c,d){var e=this._wrapper._args(arguments,['result','refs']);var f=this._wrapper._makeNode(e.parent,'feMerge',$.extend({result:e.result},e.settings||{}));for(var i=0;i<e.refs.length;i++){this._wrapper._makeNode(f,'feMergeNode',{in_:e.refs[i]})}return f},morphology:function(a,b,c,d,e,f,g){var h=this._wrapper._args(arguments,['result','in1','operator','radiusX','radiusY'],['radiusY']);return this._wrapper._makeNode(h.parent,'feMorphology',$.extend({result:h.result,in_:h.in1,operator:h.operator,radius:h.radiusX+(h.radiusY?' '+h.radiusY:'')},h.settings||{}))},offset:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','in1','dx','dy']);return this._wrapper._makeNode(g.parent,'feOffset',$.extend({result:g.result,in_:g.in1,dx:g.dx,dy:g.dy},g.settings||{}))},specularLighting:function(a,b,c,d,e,f,g){var h=this._wrapper._args(arguments,['result','in1','surfaceScale','specularConstant','specularExponent'],['surfaceScale','specularConstant','specularExponent']);return this._wrapper._makeNode(h.parent,'feSpecularLighting',$.extend({result:h.result,in_:h.in1,surfaceScale:h.surfaceScale,specularConstant:h.specularConstant,specularExponent:h.specularExponent},h.settings||{}))},tile:function(a,b,c,x,y,d,e,f){var g=this._wrapper._args(arguments,['result','in1','x','y','width','height']);return this._wrapper._makeNode(g.parent,'feTile',$.extend({result:g.result,in_:g.in1,x:g.x,y:g.y,width:g.width,height:g.height},g.settings||{}))},turbulence:function(a,b,c,d,e,f){var g=this._wrapper._args(arguments,['result','type','baseFreq','octaves'],['octaves']);return this._wrapper._makeNode(g.parent,'feTurbulence',$.extend({result:g.result,type:g.type,baseFrequency:g.baseFreq,numOctaves:g.octaves},g.settings||{}))}});function isArray(a){return(a&&a.constructor==Array)}})(jQuery)
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/icons/file.png b/azkaban-webserver/src/web/js/jquery/icons/file.png
new file mode 100644
index 0000000..0e88c1c
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/icons/file.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/icons/flow.png b/azkaban-webserver/src/web/js/jquery/icons/flow.png
new file mode 100644
index 0000000..f712774
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/icons/flow.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/icons/folderopen.png b/azkaban-webserver/src/web/js/jquery/icons/folderopen.png
new file mode 100644
index 0000000..95a8e7d
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/icons/folderopen.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/icons/job.png b/azkaban-webserver/src/web/js/jquery/icons/job.png
new file mode 100644
index 0000000..88e3f70
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/icons/job.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.autocomplete.css b/azkaban-webserver/src/web/js/jquery/jquery.autocomplete.css
new file mode 100644
index 0000000..91b6228
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.autocomplete.css
@@ -0,0 +1,48 @@
+.ac_results {
+ padding: 0px;
+ border: 1px solid black;
+ background-color: white;
+ overflow: hidden;
+ z-index: 99999;
+}
+
+.ac_results ul {
+ width: 100%;
+ list-style-position: outside;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.ac_results li {
+ margin: 0px;
+ padding: 2px 5px;
+ cursor: default;
+ display: block;
+ /*
+ if width will be 100% horizontal scrollbar will apear
+ when scroll mode will be used
+ */
+ /*width: 100%;*/
+ font: menu;
+ font-size: 12px;
+ /*
+ it is very important, if line-height not setted or setted
+ in relative units scroll will be broken in firefox
+ */
+ line-height: 16px;
+ overflow: hidden;
+}
+
+.ac_loading {
+ background: white url('indicator.gif') right center no-repeat;
+}
+
+.ac_odd {
+ background-color: #eee;
+}
+
+.ac_over {
+ background-color: #0A246A;
+ color: white;
+}
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.autocomplete.pack.js b/azkaban-webserver/src/web/js/jquery/jquery.autocomplete.pack.js
new file mode 100644
index 0000000..2d09b00
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.autocomplete.pack.js
@@ -0,0 +1,12 @@
+/*
+ * jQuery Autocomplete plugin 1.1
+ *
+ * Copyright (c) 2009 Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $
+ */
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(3($){$.2e.1u({19:3(b,d){5 c=W b=="1B";d=$.1u({},$.M.1T,{Y:c?b:P,y:c?P:b,1J:c?$.M.1T.1J:10,X:d&&!d.1D?10:48},d);d.1y=d.1y||3(a){6 a};d.1v=d.1v||d.1R;6 A.I(3(){1M $.M(A,d)})},L:3(a){6 A.11("L",a)},1k:3(a){6 A.14("1k",[a])},2b:3(){6 A.14("2b")},28:3(a){6 A.14("28",[a])},24:3(){6 A.14("24")}});$.M=3(o,r){5 t={2Y:38,2S:40,2N:46,2I:9,2E:13,2B:27,2x:3I,2v:33,2p:34,2n:8};5 u=$(o).3r("19","3o").Q(r.2Q);5 p;5 m="";5 n=$.M.3c(r);5 s=0;5 k;5 h={1F:C};5 l=$.M.32(r,o,1Z,h);5 j;$.1Y.2X&&$(o.2U).11("45.19",3(){4(j){j=C;6 C}});u.11(($.1Y.2X?"43":"42")+".19",3(a){s=1;k=a.2M;3V(a.2M){O t.2Y:a.1d();4(l.N()){l.30()}w{12(0,D)}R;O t.2S:a.1d();4(l.N()){l.2D()}w{12(0,D)}R;O t.2v:a.1d();4(l.N()){l.2C()}w{12(0,D)}R;O t.2p:a.1d();4(l.N()){l.2A()}w{12(0,D)}R;O r.17&&$.1c(r.S)==","&&t.2x:O t.2I:O t.2E:4(1Z()){a.1d();j=D;6 C}R;O t.2B:l.Z();R;3J:1P(p);p=1O(12,r.1J);R}}).2t(3(){s++}).3E(3(){s=0;4(!h.1F){2r()}}).2q(3(){4(s++>1&&!l.N()){12(0,D)}}).11("1k",3(){5 c=(1r.7>1)?1r[1]:P;3 1N(q,a){5 b;4(a&&a.7){16(5 i=0;i<a.7;i++){4(a[i].L.J()==q.J()){b=a[i];R}}}4(W c=="3")c(b);w u.14("L",b&&[b.y,b.F])}$.I(15(u.K()),3(i,a){21(a,1N,1N)})}).11("2b",3(){n.1o()}).11("28",3(){$.1u(r,1r[1]);4("y"2h 1r[1])n.1e()}).11("24",3(){l.1p();u.1p();$(o.2U).1p(".19")});3 1Z(){5 e=l.2g();4(!e)6 C;5 v=e.L;m=v;4(r.17){5 b=15(u.K());4(b.7>1){5 f=r.S.7;5 c=$(o).18().1I;5 d,1H=0;$.I(b,3(i,a){1H+=a.7;4(c<=1H){d=i;6 C}1H+=f});b[d]=v;v=b.3f(r.S)}v+=r.S}u.K(v);1l();u.14("L",[e.y,e.F]);6 D}3 12(b,c){4(k==t.2N){l.Z();6}5 a=u.K();4(!c&&a==m)6;m=a;a=1m(a);4(a.7>=r.29){u.Q(r.26);4(!r.1s)a=a.J();21(a,3a,1l)}w{1q();l.Z()}};3 15(b){4(!b)6[""];4(!r.17)6[$.1c(b)];6 $.4h(b.23(r.S),3(a){6 $.1c(b).7?$.1c(a):P})}3 1m(a){4(!r.17)6 a;5 c=15(a);4(c.7==1)6 c[0];5 b=$(o).18().1I;4(b==a.7){c=15(a)}w{c=15(a.22(a.37(b),""))}6 c[c.7-1]}3 1G(q,a){4(r.1G&&(1m(u.K()).J()==q.J())&&k!=t.2n){u.K(u.K()+a.37(1m(m).7));$(o).18(m.7,m.7+a.7)}};3 2r(){1P(p);p=1O(1l,4g)};3 1l(){5 c=l.N();l.Z();1P(p);1q();4(r.36){u.1k(3(a){4(!a){4(r.17){5 b=15(u.K()).1n(0,-1);u.K(b.3f(r.S)+(b.7?r.S:""))}w{u.K("");u.14("L",P)}}})}};3 3a(q,a){4(a&&a.7&&s){1q();l.35(a,q);1G(q,a[0].F);l.20()}w{1l()}};3 21(f,d,g){4(!r.1s)f=f.J();5 e=n.31(f);4(e&&e.7){d(f,e)}w 4((W r.Y=="1B")&&(r.Y.7>0)){5 c={4f:+1M 4e()};$.I(r.2Z,3(a,b){c[a]=W b=="3"?b():b});$.4d({4c:"4b",4a:"19"+o.49,2V:r.2V,Y:r.Y,y:$.1u({q:1m(f),47:r.X},c),44:3(a){5 b=r.1A&&r.1A(a)||1A(a);n.1i(f,b);d(f,b)}})}w{l.2T();g(f)}};3 1A(c){5 d=[];5 b=c.23("\\n");16(5 i=0;i<b.7;i++){5 a=$.1c(b[i]);4(a){a=a.23("|");d[d.7]={y:a,F:a[0],L:r.1z&&r.1z(a,a[0])||a[0]}}}6 d};3 1q(){u.1h(r.26)}};$.M.1T={2Q:"41",2P:"3Z",26:"3Y",29:1,1J:3W,1s:C,1f:D,1w:C,1g:10,X:3U,36:C,2Z:{},1X:D,1R:3(a){6 a[0]},1v:P,1G:C,E:0,17:C,S:", ",1y:3(b,a){6 b.22(1M 3T("(?![^&;]+;)(?!<[^<>]*)("+a.22(/([\\^\\$\\(\\)\\[\\]\\{\\}\\*\\.\\+\\?\\|\\\\])/2K,"\\\\$1")+")(?![^<>]*>)(?![^&;]+;)","2K"),"<2J>$1</2J>")},1D:D,1E:3S};$.M.3c=3(g){5 h={};5 j=0;3 1f(s,a){4(!g.1s)s=s.J();5 i=s.2H(a);4(g.1w=="3R"){i=s.J().1k("\\\\b"+a.J())}4(i==-1)6 C;6 i==0||g.1w};3 1i(q,a){4(j>g.1g){1o()}4(!h[q]){j++}h[q]=a}3 1e(){4(!g.y)6 C;5 f={},2G=0;4(!g.Y)g.1g=1;f[""]=[];16(5 i=0,2F=g.y.7;i<2F;i++){5 c=g.y[i];c=(W c=="1B")?[c]:c;5 d=g.1v(c,i+1,g.y.7);4(d===C)1V;5 e=d.3Q(0).J();4(!f[e])f[e]=[];5 b={F:d,y:c,L:g.1z&&g.1z(c)||d};f[e].1U(b);4(2G++<g.X){f[""].1U(b)}};$.I(f,3(i,a){g.1g++;1i(i,a)})}1O(1e,25);3 1o(){h={};j=0}6{1o:1o,1i:1i,1e:1e,31:3(q){4(!g.1g||!j)6 P;4(!g.Y&&g.1w){5 a=[];16(5 k 2h h){4(k.7>0){5 c=h[k];$.I(c,3(i,x){4(1f(x.F,q)){a.1U(x)}})}}6 a}w 4(h[q]){6 h[q]}w 4(g.1f){16(5 i=q.7-1;i>=g.29;i--){5 c=h[q.3O(0,i)];4(c){5 a=[];$.I(c,3(i,x){4(1f(x.F,q)){a[a.7]=x}});6 a}}}6 P}}};$.M.32=3(e,g,f,k){5 h={H:"3N"};5 j,z=-1,y,1t="",1S=D,G,B;3 2y(){4(!1S)6;G=$("<3M/>").Z().Q(e.2P).T("3L","3K").1Q(1K.2w);B=$("<3H/>").1Q(G).3G(3(a){4(U(a).2u&&U(a).2u.3F()==\'2s\'){z=$("1L",B).1h(h.H).3D(U(a));$(U(a)).Q(h.H)}}).2q(3(a){$(U(a)).Q(h.H);f();g.2t();6 C}).3C(3(){k.1F=D}).3B(3(){k.1F=C});4(e.E>0)G.T("E",e.E);1S=C}3 U(a){5 b=a.U;3A(b&&b.3z!="2s")b=b.3y;4(!b)6[];6 b}3 V(b){j.1n(z,z+1).1h(h.H);2o(b);5 a=j.1n(z,z+1).Q(h.H);4(e.1D){5 c=0;j.1n(0,z).I(3(){c+=A.1a});4((c+a[0].1a-B.1b())>B[0].3x){B.1b(c+a[0].1a-B.3w())}w 4(c<B.1b()){B.1b(c)}}};3 2o(a){z+=a;4(z<0){z=j.1j()-1}w 4(z>=j.1j()){z=0}}3 2m(a){6 e.X&&e.X<a?e.X:a}3 2l(){B.2z();5 b=2m(y.7);16(5 i=0;i<b;i++){4(!y[i])1V;5 a=e.1R(y[i].y,i+1,b,y[i].F,1t);4(a===C)1V;5 c=$("<1L/>").3v(e.1y(a,1t)).Q(i%2==0?"3u":"3P").1Q(B)[0];$.y(c,"2k",y[i])}j=B.3t("1L");4(e.1X){j.1n(0,1).Q(h.H);z=0}4($.2e.2W)B.2W()}6{35:3(d,q){2y();y=d;1t=q;2l()},2D:3(){V(1)},30:3(){V(-1)},2C:3(){4(z!=0&&z-8<0){V(-z)}w{V(-8)}},2A:3(){4(z!=j.1j()-1&&z+8>j.1j()){V(j.1j()-1-z)}w{V(8)}},Z:3(){G&&G.Z();j&&j.1h(h.H);z=-1},N:3(){6 G&&G.3s(":N")},3q:3(){6 A.N()&&(j.2j("."+h.H)[0]||e.1X&&j[0])},20:3(){5 a=$(g).3p();G.T({E:W e.E=="1B"||e.E>0?e.E:$(g).E(),2i:a.2i+g.1a,1W:a.1W}).20();4(e.1D){B.1b(0);B.T({2L:e.1E,3n:\'3X\'});4($.1Y.3m&&W 1K.2w.3l.2L==="1x"){5 c=0;j.I(3(){c+=A.1a});5 b=c>e.1E;B.T(\'3k\',b?e.1E:c);4(!b){j.E(B.E()-2R(j.T("2O-1W"))-2R(j.T("2O-3j")))}}}},2g:3(){5 a=j&&j.2j("."+h.H).1h(h.H);6 a&&a.7&&$.y(a[0],"2k")},2T:3(){B&&B.2z()},1p:3(){G&&G.3i()}}};$.2e.18=3(b,f){4(b!==1x){6 A.I(3(){4(A.2d){5 a=A.2d();4(f===1x||b==f){a.4n("2c",b);a.3h()}w{a.4m(D);a.4l("2c",b);a.4k("2c",f);a.3h()}}w 4(A.3g){A.3g(b,f)}w 4(A.1C){A.1C=b;A.3e=f}})}5 c=A[0];4(c.2d){5 e=1K.18.4j(),3d=c.F,2a="<->",2f=e.3b.7;e.3b=2a;5 d=c.F.2H(2a);c.F=3d;A.18(d,d+2f);6{1I:d,39:d+2f}}w 4(c.1C!==1x){6{1I:c.1C,39:c.3e}}}})(4i);',62,272,'|||function|if|var|return|length|||||||||||||||||||||||||else||data|active|this|list|false|true|width|value|element|ACTIVE|each|toLowerCase|val|result|Autocompleter|visible|case|null|addClass|break|multipleSeparator|css|target|moveSelect|typeof|max|url|hide||bind|onChange||trigger|trimWords|for|multiple|selection|autocomplete|offsetHeight|scrollTop|trim|preventDefault|populate|matchSubset|cacheLength|removeClass|add|size|search|hideResultsNow|lastWord|slice|flush|unbind|stopLoading|arguments|matchCase|term|extend|formatMatch|matchContains|undefined|highlight|formatResult|parse|string|selectionStart|scroll|scrollHeight|mouseDownOnSelect|autoFill|progress|start|delay|document|li|new|findValueCallback|setTimeout|clearTimeout|appendTo|formatItem|needsInit|defaults|push|continue|left|selectFirst|browser|selectCurrent|show|request|replace|split|unautocomplete||loadingClass||setOptions|minChars|teststring|flushCache|character|createTextRange|fn|textLength|selected|in|top|filter|ac_data|fillList|limitNumberOfItems|BACKSPACE|movePosition|PAGEDOWN|click|hideResults|LI|focus|nodeName|PAGEUP|body|COMMA|init|empty|pageDown|ESC|pageUp|next|RETURN|ol|nullData|indexOf|TAB|strong|gi|maxHeight|keyCode|DEL|padding|resultsClass|inputClass|parseInt|DOWN|emptyList|form|dataType|bgiframe|opera|UP|extraParams|prev|load|Select|||display|mustMatch|substring||end|receiveData|text|Cache|orig|selectionEnd|join|setSelectionRange|select|remove|right|height|style|msie|overflow|off|offset|current|attr|is|find|ac_even|html|innerHeight|clientHeight|parentNode|tagName|while|mouseup|mousedown|index|blur|toUpperCase|mouseover|ul|188|default|absolute|position|div|ac_over|substr|ac_odd|charAt|word|180|RegExp|100|switch|400|auto|ac_loading|ac_results||ac_input|keydown|keypress|success|submit||limit|150|name|port|abort|mode|ajax|Date|timestamp|200|map|jQuery|createRange|moveEnd|moveStart|collapse|move'.split('|'),0,{}))
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.contextMenu.css b/azkaban-webserver/src/web/js/jquery/jquery.contextMenu.css
new file mode 100644
index 0000000..229349a
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.contextMenu.css
@@ -0,0 +1,62 @@
+/* Generic context menu styles */
+.contextMenu {
+ position: absolute;
+ width: 250px;
+ z-index: 99999;
+ border: solid 1px #CCC;
+ background: #EEE;
+ padding: 0px;
+ margin: 0px;
+ display: none;
+}
+
+.contextMenu li {
+ list-style: none;
+ padding: 0px;
+ margin: 0px;
+}
+
+.contextMenu a {
+ color: #333;
+ text-decoration: none;
+ display: block;
+ line-height: 20px;
+ height: 20px;
+ background-position: 6px center;
+ background-repeat: no-repeat;
+ outline: none;
+ padding: 1px 5px;
+ padding-left: 28px;
+}
+
+.contextMenu li.hover a {
+ color: #FFF;
+ background-color: #3399FF;
+}
+
+.contextMenu LI.disabled A {
+ color: #AAA;
+ cursor: default;
+}
+
+.contextMenu LI.hover.disabled A {
+ background-color: transparent;
+}
+
+.contextMenu LI.separator {
+ border-top: solid 1px #CCC;
+}
+
+/*
+ Adding Icons
+
+ You can add icons to the context menu by adding
+ classes to the respective LI element(s)
+*/
+
+.contextMenu LI.edit A { background-image: url(images/page_white_edit.png); }
+.contextMenu LI.cut A { background-image: url(images/cut.png); }
+.contextMenu LI.copy A { background-image: url(images/page_white_copy.png); }
+.contextMenu LI.paste A { background-image: url(images/page_white_paste.png); }
+.contextMenu LI.delete A { background-image: url(images/page_white_delete.png); }
+.contextMenu LI.quit A { background-image: url(images/door.png); }
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.contextMenu.js b/azkaban-webserver/src/web/js/jquery/jquery.contextMenu.js
new file mode 100644
index 0000000..dea2e41
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.contextMenu.js
@@ -0,0 +1,217 @@
+// jQuery Context Menu Plugin
+//
+// Version 1.01
+//
+// Cory S.N. LaViska
+// A Beautiful Site (http://abeautifulsite.net/)
+//
+// More info: http://abeautifulsite.net/2008/09/jquery-context-menu-plugin/
+//
+// Terms of Use
+//
+// This plugin is dual-licensed under the GNU General Public License
+// and the MIT License and is copyright A Beautiful Site, LLC.
+//
+if(jQuery)( function() {
+ $.extend($.fn, {
+
+ contextMenu: function(o, callback) {
+ // Defaults
+ if( o.menu == undefined ) return false;
+ if( o.inSpeed == undefined ) o.inSpeed = 150;
+ if( o.outSpeed == undefined ) o.outSpeed = 75;
+ // 0 needs to be -1 for expected results (no fade)
+ if( o.inSpeed == 0 ) o.inSpeed = -1;
+ if( o.outSpeed == 0 ) o.outSpeed = -1;
+ // Loop each context menu
+ $(this).each( function() {
+ var el = $(this);
+ var offset = $(el).offset();
+ // Add contextMenu class
+ $('#' + o.menu).addClass('contextMenu');
+ // Simulate a true right click
+ $(this).mousedown( function(e) {
+ var evt = e;
+ if( evt.button == 2 ) {
+ evt.stopPropagation();
+ }
+
+ $(this).mouseup( function(e) {
+ if( e.button == 2 ) {
+ e.stopPropagation();
+ }
+
+ var srcElement = $(this);
+ $(this).unbind('mouseup');
+ if( evt.button == 2 ) {
+ // Hide context menus that may be showing
+ $(".contextMenu").hide();
+ // Get this context menu
+ var menu = $('#' + o.menu);
+
+ if( $(el).hasClass('disabled') ) return false;
+
+ // Detect mouse position
+ var d = {}, x, y;
+ if( self.innerHeight ) {
+ d.pageYOffset = self.pageYOffset;
+ d.pageXOffset = self.pageXOffset;
+ d.innerHeight = self.innerHeight;
+ d.innerWidth = self.innerWidth;
+ } else if( document.documentElement &&
+ document.documentElement.clientHeight ) {
+ d.pageYOffset = document.documentElement.scrollTop;
+ d.pageXOffset = document.documentElement.scrollLeft;
+ d.innerHeight = document.documentElement.clientHeight;
+ d.innerWidth = document.documentElement.clientWidth;
+ } else if( document.body ) {
+ d.pageYOffset = document.body.scrollTop;
+ d.pageXOffset = document.body.scrollLeft;
+ d.innerHeight = document.body.clientHeight;
+ d.innerWidth = document.body.clientWidth;
+ }
+ (e.pageX) ? x = e.pageX : x = e.clientX + d.scrollLeft;
+ (e.pageY) ? y = e.pageY : y = e.clientY + d.scrollTop;
+
+ // Show the menu
+ $(document).unbind('click');
+ $(menu).css({ top: y, left: x }).fadeIn(o.inSpeed);
+ // Hover events
+ $(menu).find('a').mouseover( function() {
+ $(menu).find('li.hover').removeClass('hover');
+ $(this).parent().addClass('hover');
+ }).mouseout( function() {
+ $(menu).find('li.hover').removeClass('hover');
+ });
+
+ // Keyboard
+ $(document).keypress( function(e) {
+ switch( e.keyCode ) {
+ case 38: // up
+ if( $(menu).find('li.hover').size() == 0 ) {
+ $(menu).find('li:last').addClass('hover');
+ } else {
+ $(menu).find('li.hover').removeClass('hover').prevAll('li:not(.disabled)').eq(0).addClass('hover');
+ if( $(menu).find('li.hover').size() == 0 ) $(menu).find('li:last').addClass('hover');
+ }
+ break;
+ case 40: // down
+ if( $(menu).find('li.hover').size() == 0 ) {
+ $(menu).find('li:first').addClass('hover');
+ } else {
+ $(menu).find('li.hover').removeClass('hover').nextAll('li:not(.disabled)').eq(0).addClass('hover');
+ if( $(menu).find('li.hover').size() == 0 ) $(menu).find('li:first').addClass('hover');
+ }
+ break;
+ case 13: // enter
+ $(menu).find('li.hover a').trigger('click');
+ break;
+ case 27: // esc
+ $(document).trigger('click');
+ break
+ }
+ });
+
+ // When items are selected
+ $('#' + o.menu).find('a').unbind('click');
+ $('#' + o.menu).find('li:not(.disabled) a').click( function() {
+ $(document).unbind('click').unbind('keypress');
+ $(".contextMenu").hide();
+ // Callback
+ if( callback ) callback( $(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y} );
+ return false;
+ });
+
+ // Hide bindings
+ setTimeout( function() { // Delay for Mozilla
+ $(document).click( function() {
+ $(document).unbind('click').unbind('keypress');
+ $(menu).fadeOut(o.outSpeed);
+ return false;
+ });
+ }, 0);
+ }
+ });
+ });
+
+ // Disable text selection
+ if( $.browser.mozilla ) {
+ $('#' + o.menu).each( function() { $(this).css({ 'MozUserSelect' : 'none' }); });
+ } else if( $.browser.msie ) {
+ $('#' + o.menu).each( function() { $(this).bind('selectstart.disableTextSelect', function() { return false; }); });
+ } else {
+ $('#' + o.menu).each(function() { $(this).bind('mousedown.disableTextSelect', function() { return false; }); });
+ }
+ // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome)
+ $(el).add($('UL.contextMenu')).bind('contextmenu', function() { return false; });
+
+ });
+ return $(this);
+ },
+
+ // Disable context menu items on the fly
+ disableContextMenuItems: function(o) {
+ if( o == undefined ) {
+ // Disable all
+ $(this).find('li').addClass('disabled');
+ return( $(this) );
+ }
+ $(this).each( function() {
+ if( o != undefined ) {
+ var d = o.split(',');
+ for( var i = 0; i < d.length; i++ ) {
+ $(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled');
+
+ }
+ }
+ });
+ return( $(this) );
+ },
+
+ // Enable context menu items on the fly
+ enableContextMenuItems: function(o) {
+ if( o == undefined ) {
+ // Enable all
+ $(this).find('li.disabled').removeClass('disabled');
+ return( $(this) );
+ }
+ $(this).each( function() {
+ if( o != undefined ) {
+ var d = o.split(',');
+ for( var i = 0; i < d.length; i++ ) {
+ $(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled');
+
+ }
+ }
+ });
+ return( $(this) );
+ },
+
+ // Disable context menu(s)
+ disableContextMenu: function() {
+ $(this).each( function() {
+ $(this).addClass('disabled');
+ });
+ return( $(this) );
+ },
+
+ // Enable context menu(s)
+ enableContextMenu: function() {
+ $(this).each( function() {
+ $(this).removeClass('disabled');
+ });
+ return( $(this) );
+ },
+
+ // Destroy context menu(s)
+ destroyContextMenu: function() {
+ // Destroy specified context menus
+ $(this).each( function() {
+ // Disable action
+ $(this).unbind('mousedown').unbind('mouseup');
+ });
+ return( $(this) );
+ }
+
+ });
+})(jQuery);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.cookie.js b/azkaban-webserver/src/web/js/jquery/jquery.cookie.js
new file mode 100644
index 0000000..6036754
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.cookie.js
@@ -0,0 +1,96 @@
+/**
+ * Cookie plugin
+ *
+ * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ */
+
+/**
+ * Create a cookie with the given name and value and other optional parameters.
+ *
+ * @example $.cookie('the_cookie', 'the_value');
+ * @desc Set the value of a cookie.
+ * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
+ * @desc Create a cookie with all available options.
+ * @example $.cookie('the_cookie', 'the_value');
+ * @desc Create a session cookie.
+ * @example $.cookie('the_cookie', null);
+ * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
+ * used when the cookie was set.
+ *
+ * @param String name The name of the cookie.
+ * @param String value The value of the cookie.
+ * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
+ * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
+ * If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
+ * If set to null or omitted, the cookie will be a session cookie and will not be retained
+ * when the the browser exits.
+ * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
+ * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
+ * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
+ * require a secure protocol (like HTTPS).
+ * @type undefined
+ *
+ * @name $.cookie
+ * @cat Plugins/Cookie
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
+ */
+
+/**
+ * Get the value of a cookie with the given name.
+ *
+ * @example $.cookie('the_cookie');
+ * @desc Get the value of a cookie.
+ *
+ * @param String name The name of the cookie.
+ * @return The value of the cookie.
+ * @type String
+ *
+ * @name $.cookie
+ * @cat Plugins/Cookie
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
+ */
+jQuery.cookie = function(name, value, options) {
+ if (typeof value != 'undefined') { // name and value given, set cookie
+ options = options || {};
+ if (value === null) {
+ value = '';
+ options.expires = -1;
+ }
+ var expires = '';
+ if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
+ var date;
+ if (typeof options.expires == 'number') {
+ date = new Date();
+ date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
+ } else {
+ date = options.expires;
+ }
+ expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
+ }
+ // CAUTION: Needed to parenthesize options.path and options.domain
+ // in the following expressions, otherwise they evaluate to undefined
+ // in the packed version for some reason...
+ var path = options.path ? '; path=' + (options.path) : '';
+ var domain = options.domain ? '; domain=' + (options.domain) : '';
+ var secure = options.secure ? '; secure' : '';
+ document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
+ } else { // only name given, get cookie
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+};
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.hotkeys.js b/azkaban-webserver/src/web/js/jquery/jquery.hotkeys.js
new file mode 100644
index 0000000..cd62905
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.hotkeys.js
@@ -0,0 +1,99 @@
+/*
+ * jQuery Hotkeys Plugin
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ *
+ * Based upon the plugin by Tzury Bar Yochay:
+ * http://github.com/tzuryby/hotkeys
+ *
+ * Original idea by:
+ * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
+*/
+
+(function(jQuery){
+
+ jQuery.hotkeys = {
+ version: "0.8",
+
+ specialKeys: {
+ 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
+ 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
+ 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
+ 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
+ 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
+ 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
+ 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
+ },
+
+ shiftNums: {
+ "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
+ "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
+ ".": ">", "/": "?", "\\": "|"
+ }
+ };
+
+ function keyHandler( handleObj ) {
+ // Only care when a possible input has been specified
+ if ( typeof handleObj.data !== "string" ) {
+ return;
+ }
+
+ var origHandler = handleObj.handler,
+ keys = handleObj.data.toLowerCase().split(" ");
+
+ handleObj.handler = function( event ) {
+ // Don't fire in text-accepting inputs that we didn't directly bind to
+ if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) ||
+ event.target.type === "text") ) {
+ return;
+ }
+
+ // Keypress represents characters, not special keys
+ var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ],
+ character = String.fromCharCode( event.which ).toLowerCase(),
+ key, modif = "", possible = {};
+
+ // check combinations (alt|ctrl|shift+anything)
+ if ( event.altKey && special !== "alt" ) {
+ modif += "alt+";
+ }
+
+ if ( event.ctrlKey && special !== "ctrl" ) {
+ modif += "ctrl+";
+ }
+
+ // TODO: Need to make sure this works consistently across platforms
+ if ( event.metaKey && !event.ctrlKey && special !== "meta" ) {
+ modif += "meta+";
+ }
+
+ if ( event.shiftKey && special !== "shift" ) {
+ modif += "shift+";
+ }
+
+ if ( special ) {
+ possible[ modif + special ] = true;
+
+ } else {
+ possible[ modif + character ] = true;
+ possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true;
+
+ // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
+ if ( modif === "shift+" ) {
+ possible[ jQuery.hotkeys.shiftNums[ character ] ] = true;
+ }
+ }
+
+ for ( var i = 0, l = keys.length; i < l; i++ ) {
+ if ( possible[ keys[i] ] ) {
+ return origHandler.apply( this, arguments );
+ }
+ }
+ };
+ }
+
+ jQuery.each([ "keydown", "keyup", "keypress" ], function() {
+ jQuery.event.special[ this ] = { add: keyHandler };
+ });
+
+})( jQuery );
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.svg.min.js b/azkaban-webserver/src/web/js/jquery/jquery.svg.min.js
new file mode 100644
index 0000000..5b922fb
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.svg.min.js
@@ -0,0 +1,7 @@
+/* http://keith-wood.name/svg.html
+ SVG for jQuery v1.4.5.
+ Written by Keith Wood (kbwood{at}iinet.com.au) August 2007.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+(function($){function SVGManager(){this._settings=[];this._extensions=[];this.regional=[];this.regional['']={errorLoadingText:'Error loading',notSupportedText:'This browser does not support SVG'};this.local=this.regional[''];this._uuid=new Date().getTime();this._renesis=detectActiveX('RenesisX.RenesisCtrl')}function detectActiveX(a){try{return!!(window.ActiveXObject&&new ActiveXObject(a))}catch(e){return false}}var q='svgwrapper';$.extend(SVGManager.prototype,{markerClassName:'hasSVG',svgNS:'http://www.w3.org/2000/svg',xlinkNS:'http://www.w3.org/1999/xlink',_wrapperClass:SVGWrapper,_attrNames:{class_:'class',in_:'in',alignmentBaseline:'alignment-baseline',baselineShift:'baseline-shift',clipPath:'clip-path',clipRule:'clip-rule',colorInterpolation:'color-interpolation',colorInterpolationFilters:'color-interpolation-filters',colorRendering:'color-rendering',dominantBaseline:'dominant-baseline',enableBackground:'enable-background',fillOpacity:'fill-opacity',fillRule:'fill-rule',floodColor:'flood-color',floodOpacity:'flood-opacity',fontFamily:'font-family',fontSize:'font-size',fontSizeAdjust:'font-size-adjust',fontStretch:'font-stretch',fontStyle:'font-style',fontVariant:'font-variant',fontWeight:'font-weight',glyphOrientationHorizontal:'glyph-orientation-horizontal',glyphOrientationVertical:'glyph-orientation-vertical',horizAdvX:'horiz-adv-x',horizOriginX:'horiz-origin-x',imageRendering:'image-rendering',letterSpacing:'letter-spacing',lightingColor:'lighting-color',markerEnd:'marker-end',markerMid:'marker-mid',markerStart:'marker-start',stopColor:'stop-color',stopOpacity:'stop-opacity',strikethroughPosition:'strikethrough-position',strikethroughThickness:'strikethrough-thickness',strokeDashArray:'stroke-dasharray',strokeDashOffset:'stroke-dashoffset',strokeLineCap:'stroke-linecap',strokeLineJoin:'stroke-linejoin',strokeMiterLimit:'stroke-miterlimit',strokeOpacity:'stroke-opacity',strokeWidth:'stroke-width',textAnchor:'text-anchor',textDecoration:'text-decoration',textRendering:'text-rendering',underlinePosition:'underline-position',underlineThickness:'underline-thickness',vertAdvY:'vert-adv-y',vertOriginY:'vert-origin-y',wordSpacing:'word-spacing',writingMode:'writing-mode'},_attachSVG:function(a,b){var c=(a.namespaceURI==this.svgNS?a:null);var a=(c?null:a);if($(a||c).hasClass(this.markerClassName)){return}if(typeof b=='string'){b={loadURL:b}}else if(typeof b=='function'){b={onLoad:b}}$(a||c).addClass(this.markerClassName);try{if(!c){c=document.createElementNS(this.svgNS,'svg');c.setAttribute('version','1.1');if(a.clientWidth>0){c.setAttribute('width',a.clientWidth)}if(a.clientHeight>0){c.setAttribute('height',a.clientHeight)}a.appendChild(c)}this._afterLoad(a,c,b||{})}catch(e){if($.browser.msie){if(!a.id){a.id='svg'+(this._uuid++)}this._settings[a.id]=b;a.innerHTML='<embed type="image/svg+xml" width="100%" '+'height="100%" src="'+(b.initPath||'')+'blank.svg" '+'pluginspage="http://www.adobe.com/svg/viewer/install/main.html"/>'}else{a.innerHTML='<p class="svg_error">'+this.local.notSupportedText+'</p>'}}},_registerSVG:function(){for(var i=0;i<document.embeds.length;i++){var a=document.embeds[i].parentNode;if(!$(a).hasClass($.svg.markerClassName)||$.data(a,q)){continue}var b=null;try{b=document.embeds[i].getSVGDocument()}catch(e){setTimeout($.svg._registerSVG,250);return}b=(b?b.documentElement:null);if(b){$.svg._afterLoad(a,b)}}},_afterLoad:function(a,b,c){var c=c||this._settings[a.id];this._settings[a?a.id:'']=null;var d=new this._wrapperClass(b,a);$.data(a||b,q,d);try{if(c.loadURL){d.load(c.loadURL,c)}if(c.settings){d.configure(c.settings)}if(c.onLoad&&!c.loadURL){c.onLoad.apply(a||b,[d])}}catch(e){alert(e)}},_getSVG:function(a){a=(typeof a=='string'?$(a)[0]:(a.jquery?a[0]:a));return $.data(a,q)},_destroySVG:function(a){var b=$(a);if(!b.hasClass(this.markerClassName)){return}b.removeClass(this.markerClassName);if(a.namespaceURI!=this.svgNS){b.empty()}$.removeData(a,q)},addExtension:function(a,b){this._extensions.push([a,b])},isSVGElem:function(a){return(a.nodeType==1&&a.namespaceURI==$.svg.svgNS)}});function SVGWrapper(a,b){this._svg=a;this._container=b;for(var i=0;i<$.svg._extensions.length;i++){var c=$.svg._extensions[i];this[c[0]]=new c[1](this)}}$.extend(SVGWrapper.prototype,{_width:function(){return(this._container?this._container.clientWidth:this._svg.width)},_height:function(){return(this._container?this._container.clientHeight:this._svg.height)},root:function(){return this._svg},configure:function(a,b,c){if(!a.nodeName){c=b;b=a;a=this._svg}if(c){for(var i=a.attributes.length-1;i>=0;i--){var d=a.attributes.item(i);if(!(d.nodeName=='onload'||d.nodeName=='version'||d.nodeName.substring(0,5)=='xmlns')){a.attributes.removeNamedItem(d.nodeName)}}}for(var e in b){a.setAttribute($.svg._attrNames[e]||e,b[e])}return this},getElementById:function(a){return this._svg.ownerDocument.getElementById(a)},change:function(a,b){if(a){for(var c in b){if(b[c]==null){a.removeAttribute($.svg._attrNames[c]||c)}else{a.setAttribute($.svg._attrNames[c]||c,b[c])}}}return this},_args:function(b,c,d){c.splice(0,0,'parent');c.splice(c.length,0,'settings');var e={};var f=0;if(b[0]!=null&&b[0].jquery){b[0]=b[0][0]}if(b[0]!=null&&!(typeof b[0]=='object'&&b[0].nodeName)){e['parent']=null;f=1}for(var i=0;i<b.length;i++){e[c[i+f]]=b[i]}if(d){$.each(d,function(i,a){if(typeof e[a]=='object'){e.settings=e[a];e[a]=null}})}return e},title:function(a,b,c){var d=this._args(arguments,['text']);var e=this._makeNode(d.parent,'title',d.settings||{});e.appendChild(this._svg.ownerDocument.createTextNode(d.text));return e},describe:function(a,b,c){var d=this._args(arguments,['text']);var e=this._makeNode(d.parent,'desc',d.settings||{});e.appendChild(this._svg.ownerDocument.createTextNode(d.text));return e},defs:function(a,b,c){var d=this._args(arguments,['id'],['id']);return this._makeNode(d.parent,'defs',$.extend((d.id?{id:d.id}:{}),d.settings||{}))},symbol:function(a,b,c,d,e,f,g){var h=this._args(arguments,['id','x1','y1','width','height']);return this._makeNode(h.parent,'symbol',$.extend({id:h.id,viewBox:h.x1+' '+h.y1+' '+h.width+' '+h.height},h.settings||{}))},marker:function(a,b,c,d,e,f,g,h){var i=this._args(arguments,['id','refX','refY','mWidth','mHeight','orient'],['orient']);return this._makeNode(i.parent,'marker',$.extend({id:i.id,refX:i.refX,refY:i.refY,markerWidth:i.mWidth,markerHeight:i.mHeight,orient:i.orient||'auto'},i.settings||{}))},style:function(a,b,c){var d=this._args(arguments,['styles']);var e=this._makeNode(d.parent,'style',$.extend({type:'text/css'},d.settings||{}));e.appendChild(this._svg.ownerDocument.createTextNode(d.styles));if($.browser.opera){$('head').append('<style type="text/css">'+d.styles+'</style>')}return e},script:function(a,b,c,d){var e=this._args(arguments,['script','type'],['type']);var f=this._makeNode(e.parent,'script',$.extend({type:e.type||'text/javascript'},e.settings||{}));f.appendChild(this._svg.ownerDocument.createTextNode(e.script));if(!$.browser.mozilla){$.globalEval(e.script)}return f},linearGradient:function(a,b,c,d,e,f,g,h){var i=this._args(arguments,['id','stops','x1','y1','x2','y2'],['x1']);var j=$.extend({id:i.id},(i.x1!=null?{x1:i.x1,y1:i.y1,x2:i.x2,y2:i.y2}:{}));return this._gradient(i.parent,'linearGradient',$.extend(j,i.settings||{}),i.stops)},radialGradient:function(a,b,c,d,e,r,f,g,h){var i=this._args(arguments,['id','stops','cx','cy','r','fx','fy'],['cx']);var j=$.extend({id:i.id},(i.cx!=null?{cx:i.cx,cy:i.cy,r:i.r,fx:i.fx,fy:i.fy}:{}));return this._gradient(i.parent,'radialGradient',$.extend(j,i.settings||{}),i.stops)},_gradient:function(a,b,c,d){var e=this._makeNode(a,b,c);for(var i=0;i<d.length;i++){var f=d[i];this._makeNode(e,'stop',$.extend({offset:f[0],stopColor:f[1]},(f[2]!=null?{stopOpacity:f[2]}:{})))}return e},pattern:function(a,b,x,y,c,d,e,f,g,h,i){var j=this._args(arguments,['id','x','y','width','height','vx','vy','vwidth','vheight'],['vx']);var k=$.extend({id:j.id,x:j.x,y:j.y,width:j.width,height:j.height},(j.vx!=null?{viewBox:j.vx+' '+j.vy+' '+j.vwidth+' '+j.vheight}:{}));return this._makeNode(j.parent,'pattern',$.extend(k,j.settings||{}))},clipPath:function(a,b,c,d){var e=this._args(arguments,['id','units']);e.units=e.units||'userSpaceOnUse';return this._makeNode(e.parent,'clipPath',$.extend({id:e.id,clipPathUnits:e.units},e.settings||{}))},mask:function(a,b,x,y,c,d,e){var f=this._args(arguments,['id','x','y','width','height']);return this._makeNode(f.parent,'mask',$.extend({id:f.id,x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}))},createPath:function(){return new SVGPath()},createText:function(){return new SVGText()},svg:function(a,x,y,b,c,d,e,f,g,h){var i=this._args(arguments,['x','y','width','height','vx','vy','vwidth','vheight'],['vx']);var j=$.extend({x:i.x,y:i.y,width:i.width,height:i.height},(i.vx!=null?{viewBox:i.vx+' '+i.vy+' '+i.vwidth+' '+i.vheight}:{}));return this._makeNode(i.parent,'svg',$.extend(j,i.settings||{}))},group:function(a,b,c){var d=this._args(arguments,['id'],['id']);return this._makeNode(d.parent,'g',$.extend({id:d.id},d.settings||{}))},use:function(a,x,y,b,c,d,e){var f=this._args(arguments,['x','y','width','height','ref']);if(typeof f.x=='string'){f.ref=f.x;f.settings=f.y;f.x=f.y=f.width=f.height=null}var g=this._makeNode(f.parent,'use',$.extend({x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}));g.setAttributeNS($.svg.xlinkNS,'href',f.ref);return g},link:function(a,b,c){var d=this._args(arguments,['ref']);var e=this._makeNode(d.parent,'a',d.settings);e.setAttributeNS($.svg.xlinkNS,'href',d.ref);return e},image:function(a,x,y,b,c,d,e){var f=this._args(arguments,['x','y','width','height','ref']);var g=this._makeNode(f.parent,'image',$.extend({x:f.x,y:f.y,width:f.width,height:f.height},f.settings||{}));g.setAttributeNS($.svg.xlinkNS,'href',f.ref);return g},path:function(a,b,c){var d=this._args(arguments,['path']);return this._makeNode(d.parent,'path',$.extend({d:(d.path.path?d.path.path():d.path)},d.settings||{}))},rect:function(a,x,y,b,c,d,e,f){var g=this._args(arguments,['x','y','width','height','rx','ry'],['rx']);return this._makeNode(g.parent,'rect',$.extend({x:g.x,y:g.y,width:g.width,height:g.height},(g.rx?{rx:g.rx,ry:g.ry}:{}),g.settings||{}))},circle:function(a,b,c,r,d){var e=this._args(arguments,['cx','cy','r']);return this._makeNode(e.parent,'circle',$.extend({cx:e.cx,cy:e.cy,r:e.r},e.settings||{}))},ellipse:function(a,b,c,d,e,f){var g=this._args(arguments,['cx','cy','rx','ry']);return this._makeNode(g.parent,'ellipse',$.extend({cx:g.cx,cy:g.cy,rx:g.rx,ry:g.ry},g.settings||{}))},line:function(a,b,c,d,e,f){var g=this._args(arguments,['x1','y1','x2','y2']);return this._makeNode(g.parent,'line',$.extend({x1:g.x1,y1:g.y1,x2:g.x2,y2:g.y2},g.settings||{}))},polyline:function(a,b,c){var d=this._args(arguments,['points']);return this._poly(d.parent,'polyline',d.points,d.settings)},polygon:function(a,b,c){var d=this._args(arguments,['points']);return this._poly(d.parent,'polygon',d.points,d.settings)},_poly:function(a,b,c,d){var e='';for(var i=0;i<c.length;i++){e+=c[i].join()+' '}return this._makeNode(a,b,$.extend({points:$.trim(e)},d||{}))},text:function(a,x,y,b,c){var d=this._args(arguments,['x','y','value']);if(typeof d.x=='string'&&arguments.length<4){d.value=d.x;d.settings=d.y;d.x=d.y=null}return this._text(d.parent,'text',d.value,$.extend({x:(d.x&&isArray(d.x)?d.x.join(' '):d.x),y:(d.y&&isArray(d.y)?d.y.join(' '):d.y)},d.settings||{}))},textpath:function(a,b,c,d){var e=this._args(arguments,['path','value']);var f=this._text(e.parent,'textPath',e.value,e.settings||{});f.setAttributeNS($.svg.xlinkNS,'href',e.path);return f},_text:function(a,b,c,d){var e=this._makeNode(a,b,d);if(typeof c=='string'){e.appendChild(e.ownerDocument.createTextNode(c))}else{for(var i=0;i<c._parts.length;i++){var f=c._parts[i];if(f[0]=='tspan'){var g=this._makeNode(e,f[0],f[2]);g.appendChild(e.ownerDocument.createTextNode(f[1]));e.appendChild(g)}else if(f[0]=='tref'){var g=this._makeNode(e,f[0],f[2]);g.setAttributeNS($.svg.xlinkNS,'href',f[1]);e.appendChild(g)}else if(f[0]=='textpath'){var h=$.extend({},f[2]);h.href=null;var g=this._makeNode(e,f[0],h);g.setAttributeNS($.svg.xlinkNS,'href',f[2].href);g.appendChild(e.ownerDocument.createTextNode(f[1]));e.appendChild(g)}else{e.appendChild(e.ownerDocument.createTextNode(f[1]))}}}return e},other:function(a,b,c){var d=this._args(arguments,['name']);return this._makeNode(d.parent,d.name,d.settings||{})},_makeNode:function(a,b,c){a=a||this._svg;var d=this._svg.ownerDocument.createElementNS($.svg.svgNS,b);for(var b in c){var e=c[b];if(e!=null&&e!=null&&(typeof e!='string'||e!='')){d.setAttribute($.svg._attrNames[b]||b,e)}}a.appendChild(d);return d},add:function(b,c){var d=this._args((arguments.length==1?[null,b]:arguments),['node']);var f=this;d.parent=d.parent||this._svg;d.node=(d.node.jquery?d.node:$(d.node));try{if($.svg._renesis){throw'Force traversal';}d.parent.appendChild(d.node.cloneNode(true))}catch(e){d.node.each(function(){var a=f._cloneAsSVG(this);if(a){d.parent.appendChild(a)}})}return this},clone:function(b,c){var d=this;var e=this._args((arguments.length==1?[null,b]:arguments),['node']);e.parent=e.parent||this._svg;e.node=(e.node.jquery?e.node:$(e.node));var f=[];e.node.each(function(){var a=d._cloneAsSVG(this);if(a){a.id='';e.parent.appendChild(a);f.push(a)}});return f},_cloneAsSVG:function(a){var b=null;if(a.nodeType==1){b=this._svg.ownerDocument.createElementNS($.svg.svgNS,this._checkName(a.nodeName));for(var i=0;i<a.attributes.length;i++){var c=a.attributes.item(i);if(c.nodeName!='xmlns'&&c.nodeValue){if(c.prefix=='xlink'){b.setAttributeNS($.svg.xlinkNS,c.localName||c.baseName,c.nodeValue)}else{b.setAttribute(this._checkName(c.nodeName),c.nodeValue)}}}for(var i=0;i<a.childNodes.length;i++){var d=this._cloneAsSVG(a.childNodes[i]);if(d){b.appendChild(d)}}}else if(a.nodeType==3){if($.trim(a.nodeValue)){b=this._svg.ownerDocument.createTextNode(a.nodeValue)}}else if(a.nodeType==4){if($.trim(a.nodeValue)){try{b=this._svg.ownerDocument.createCDATASection(a.nodeValue)}catch(e){b=this._svg.ownerDocument.createTextNode(a.nodeValue.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'))}}}return b},_checkName:function(a){a=(a.substring(0,1)>='A'&&a.substring(0,1)<='Z'?a.toLowerCase():a);return(a.substring(0,4)=='svg:'?a.substring(4):a)},load:function(j,k){k=(typeof k=='boolean'?{addTo:k}:(typeof k=='function'?{onLoad:k}:(typeof k=='string'?{parent:k}:(typeof k=='object'&&k.nodeName?{parent:k}:(typeof k=='object'&&k.jquery?{parent:k}:k||{})))));if(!k.parent&&!k.addTo){this.clear(false)}var l=[this._svg.getAttribute('width'),this._svg.getAttribute('height')];var m=this;var n=function(a){a=$.svg.local.errorLoadingText+': '+a;if(k.onLoad){k.onLoad.apply(m._container||m._svg,[m,a])}else{m.text(null,10,20,a)}};var o=function(a){var b=new ActiveXObject('Microsoft.XMLDOM');b.validateOnParse=false;b.resolveExternals=false;b.async=false;b.loadXML(a);if(b.parseError.errorCode!=0){n(b.parseError.reason);return null}return b};var p=function(a){if(!a){return}if(a.documentElement.nodeName!='svg'){var b=a.getElementsByTagName('parsererror');var c=(b.length?b[0].getElementsByTagName('div'):[]);n(!b.length?'???':(c.length?c[0]:b[0]).firstChild.nodeValue);return}var d=(k.parent?$(k.parent)[0]:m._svg);var f={};for(var i=0;i<a.documentElement.attributes.length;i++){var g=a.documentElement.attributes.item(i);if(!(g.nodeName=='version'||g.nodeName.substring(0,5)=='xmlns')){f[g.nodeName]=g.nodeValue}}m.configure(d,f,!k.parent);var h=a.documentElement.childNodes;for(var i=0;i<h.length;i++){try{if($.svg._renesis){throw'Force traversal';}d.appendChild(m._svg.ownerDocument.importNode(h[i],true));if(h[i].nodeName=='script'){$.globalEval(h[i].textContent)}}catch(e){m.add(d,h[i])}}if(!k.changeSize){m.configure(d,{width:l[0],height:l[1]})}if(k.onLoad){k.onLoad.apply(m._container||m._svg,[m])}};if(j.match('<svg')){p($.browser.msie?o(j):new DOMParser().parseFromString(j,'text/xml'))}else{$.ajax({url:j,dataType:($.browser.msie?'text':'xml'),success:function(a){p($.browser.msie?o(a):a)},error:function(a,b,c){n(b+(c?' '+c.message:''))}})}return this},remove:function(a){a=(a.jquery?a[0]:a);a.parentNode.removeChild(a);return this},clear:function(a){if(a){this.configure({},true)}while(this._svg.firstChild){this._svg.removeChild(this._svg.firstChild)}return this},toSVG:function(a){a=a||this._svg;return(typeof XMLSerializer=='undefined'?this._toSVG(a):new XMLSerializer().serializeToString(a))},_toSVG:function(a){var b='';if(!a){return b}if(a.nodeType==3){b=a.nodeValue}else if(a.nodeType==4){b='<![CDATA['+a.nodeValue+']]>'}else{b='<'+a.nodeName;if(a.attributes){for(var i=0;i<a.attributes.length;i++){var c=a.attributes.item(i);if(!($.trim(c.nodeValue)==''||c.nodeValue.match(/^\[object/)||c.nodeValue.match(/^function/))){b+=' '+(c.namespaceURI==$.svg.xlinkNS?'xlink:':'')+c.nodeName+'="'+c.nodeValue+'"'}}}if(a.firstChild){b+='>';var d=a.firstChild;while(d){b+=this._toSVG(d);d=d.nextSibling}b+='</'+a.nodeName+'>'}else{b+='/>'}}return b}});function SVGPath(){this._path=''}$.extend(SVGPath.prototype,{reset:function(){this._path='';return this},move:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'m':'M'),x,y)},line:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'l':'L'),x,y)},horiz:function(x,a){this._path+=(a?'h':'H')+(isArray(x)?x.join(' '):x);return this},vert:function(y,a){this._path+=(a?'v':'V')+(isArray(y)?y.join(' '):y);return this},curveC:function(a,b,c,d,x,y,e){e=(isArray(a)?b:e);return this._coords((e?'c':'C'),a,b,c,d,x,y)},smoothC:function(a,b,x,y,c){c=(isArray(a)?b:c);return this._coords((c?'s':'S'),a,b,x,y)},curveQ:function(a,b,x,y,c){c=(isArray(a)?b:c);return this._coords((c?'q':'Q'),a,b,x,y)},smoothQ:function(x,y,a){a=(isArray(x)?y:a);return this._coords((a?'t':'T'),x,y)},_coords:function(a,b,c,d,e,f,g){if(isArray(b)){for(var i=0;i<b.length;i++){var h=b[i];this._path+=(i==0?a:' ')+h[0]+','+h[1]+(h.length<4?'':' '+h[2]+','+h[3]+(h.length<6?'':' '+h[4]+','+h[5]))}}else{this._path+=a+b+','+c+(d==null?'':' '+d+','+e+(f==null?'':' '+f+','+g))}return this},arc:function(a,b,c,d,e,x,y,f){f=(isArray(a)?b:f);this._path+=(f?'a':'A');if(isArray(a)){for(var i=0;i<a.length;i++){var g=a[i];this._path+=(i==0?'':' ')+g[0]+','+g[1]+' '+g[2]+' '+(g[3]?'1':'0')+','+(g[4]?'1':'0')+' '+g[5]+','+g[6]}}else{this._path+=a+','+b+' '+c+' '+(d?'1':'0')+','+(e?'1':'0')+' '+x+','+y}return this},close:function(){this._path+='z';return this},path:function(){return this._path}});SVGPath.prototype.moveTo=SVGPath.prototype.move;SVGPath.prototype.lineTo=SVGPath.prototype.line;SVGPath.prototype.horizTo=SVGPath.prototype.horiz;SVGPath.prototype.vertTo=SVGPath.prototype.vert;SVGPath.prototype.curveCTo=SVGPath.prototype.curveC;SVGPath.prototype.smoothCTo=SVGPath.prototype.smoothC;SVGPath.prototype.curveQTo=SVGPath.prototype.curveQ;SVGPath.prototype.smoothQTo=SVGPath.prototype.smoothQ;SVGPath.prototype.arcTo=SVGPath.prototype.arc;function SVGText(){this._parts=[]}$.extend(SVGText.prototype,{reset:function(){this._parts=[];return this},string:function(a){this._parts[this._parts.length]=['text',a];return this},span:function(a,b){this._parts[this._parts.length]=['tspan',a,b];return this},ref:function(a,b){this._parts[this._parts.length]=['tref',a,b];return this},path:function(a,b,c){this._parts[this._parts.length]=['textpath',b,$.extend({href:a},c||{})];return this}});$.fn.svg=function(a){var b=Array.prototype.slice.call(arguments,1);if(typeof a=='string'&&a=='get'){return $.svg['_'+a+'SVG'].apply($.svg,[this[0]].concat(b))}return this.each(function(){if(typeof a=='string'){$.svg['_'+a+'SVG'].apply($.svg,[this].concat(b))}else{$.svg._attachSVG(this,a||{})}})};function isArray(a){return(a&&a.constructor==Array)}$.svg=new SVGManager()})(jQuery);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.tablesorter.min.js b/azkaban-webserver/src/web/js/jquery/jquery.tablesorter.min.js
new file mode 100644
index 0000000..64c7007
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.tablesorter.min.js
@@ -0,0 +1,2 @@
+
+(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,cells[i]);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,node){var l=parsers.length;for(var i=1;i<l;i++){if(parsers[i].is($.trim(getElementText(table.config,node)),table,node)){return parsers[i];}}return parsers[0];}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=table.tBodies[0].rows[i],cols=[];cache.row.push($(c));for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c.cells[j]),table,c.cells[j]));}cols.push(i);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){if(!node)return"";var t="";if(config.textExtraction=="simple"){if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){t=node.childNodes[0].innerHTML;}else{t=node.innerHTML;}}else{if(typeof(config.textExtraction)=="function"){t=config.textExtraction(node);}else{t=$(node).text();}}return t;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){rows.push(r[n[i][checkCell]]);if(!table.config.appender){var o=r[n[i][checkCell]];var l=o.length;for(var j=0;j<l;j++){tableBody[0].appendChild(o[j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false,tableHeadersRows=[];for(var i=0;i<table.tHead.rows.length;i++){tableHeadersRows[i]=0;};$tableHeaders=$("thead th",table);$tableHeaders.each(function(index){this.count=0;this.column=index;this.order=formatSortingOrder(table.config.sortInitialOrder);if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(!this.sortDisabled){$(this).addClass(table.config.cssHeader);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){i=(v.toLowerCase()=="desc")?1:0;}else{i=(v==(0||1))?v:0;}return i;}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(getCachedSortType(table.config.parsers,c)=="text")?((order==0)?"sortText":"sortTextDesc"):((order==0)?"sortNumeric":"sortNumericDesc");var e="e"+i;dynamicExp+="var "+e+" = "+s+"(a["+c+"],b["+c+"]); ";dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function sortText(a,b){return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){var DECIMAL='\\'+config.decimal;var exp='/(^[+]?0('+DECIMAL+'0+)?$)|(^([-+]?[1-9][0-9]*)$)|(^([-+]?((0?|[1-9][0-9]*)'+DECIMAL+'(0*[1-9][0-9]*)))$)|(^[-+]?[1-9]+[0-9]*'+DECIMAL+'0+$)/';return RegExp(exp).test($.trim(s));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[^0-9.]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}$("tr:visible",table.tBodies[0]).filter(':even').removeClass(table.config.widgetZebra.css[1]).addClass(table.config.widgetZebra.css[0]).end().filter(':odd').removeClass(table.config.widgetZebra.css[0]).addClass(table.config.widgetZebra.css[1]);if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.tools.min.js b/azkaban-webserver/src/web/js/jquery/jquery.tools.min.js
new file mode 100644
index 0000000..fd134c0
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.tools.min.js
@@ -0,0 +1,37 @@
+/*
+ * jQuery Tools 1.2.5 - The missing UI library for the Web
+ *
+ * [tabs, tooltip, overlay, scrollable]
+ *
+ * NO COPYRIGHTS OR LICENSES. DO WHAT YOU LIKE.
+ *
+ * http://flowplayer.org/tools/
+ *
+ * File generated: Wed Sep 22 06:12:55 GMT 2010
+ */
+(function(c){function p(d,b,a){var e=this,l=d.add(this),h=d.find(a.tabs),i=b.jquery?b:d.children(b),j;h.length||(h=d.children());i.length||(i=d.parent().find(b));i.length||(i=c(b));c.extend(this,{click:function(f,g){var k=h.eq(f);if(typeof f=="string"&&f.replace("#","")){k=h.filter("[href*="+f.replace("#","")+"]");f=Math.max(h.index(k),0)}if(a.rotate){var n=h.length-1;if(f<0)return e.click(n,g);if(f>n)return e.click(0,g)}if(!k.length){if(j>=0)return e;f=a.initialIndex;k=h.eq(f)}if(f===j)return e;
+g=g||c.Event();g.type="onBeforeClick";l.trigger(g,[f]);if(!g.isDefaultPrevented()){o[a.effect].call(e,f,function(){g.type="onClick";l.trigger(g,[f])});j=f;h.removeClass(a.current);k.addClass(a.current);return e}},getConf:function(){return a},getTabs:function(){return h},getPanes:function(){return i},getCurrentPane:function(){return i.eq(j)},getCurrentTab:function(){return h.eq(j)},getIndex:function(){return j},next:function(){return e.click(j+1)},prev:function(){return e.click(j-1)},destroy:function(){h.unbind(a.event).removeClass(a.current);
+i.find("a[href^=#]").unbind("click.T");return e}});c.each("onBeforeClick,onClick".split(","),function(f,g){c.isFunction(a[g])&&c(e).bind(g,a[g]);e[g]=function(k){k&&c(e).bind(g,k);return e}});if(a.history&&c.fn.history){c.tools.history.init(h);a.event="history"}h.each(function(f){c(this).bind(a.event,function(g){e.click(f,g);return g.preventDefault()})});i.find("a[href^=#]").bind("click.T",function(f){e.click(c(this).attr("href"),f)});if(location.hash&&a.tabs=="a"&&d.find("[href="+location.hash+"]").length)e.click(location.hash);
+else if(a.initialIndex===0||a.initialIndex>0)e.click(a.initialIndex)}c.tools=c.tools||{version:"1.2.5"};c.tools.tabs={conf:{tabs:"a",current:"current",onBeforeClick:null,onClick:null,effect:"default",initialIndex:0,event:"click",rotate:false,history:false},addEffect:function(d,b){o[d]=b}};var o={"default":function(d,b){this.getPanes().hide().eq(d).show();b.call()},fade:function(d,b){var a=this.getConf(),e=a.fadeOutSpeed,l=this.getPanes();e?l.fadeOut(e):l.hide();l.eq(d).fadeIn(a.fadeInSpeed,b)},slide:function(d,
+b){this.getPanes().slideUp(200);this.getPanes().eq(d).slideDown(400,b)},ajax:function(d,b){this.getPanes().eq(0).load(this.getTabs().eq(d).attr("href"),b)}},m;c.tools.tabs.addEffect("horizontal",function(d,b){m||(m=this.getPanes().eq(0).width());this.getCurrentPane().animate({width:0},function(){c(this).hide()});this.getPanes().eq(d).animate({width:m},function(){c(this).show();b.call()})});c.fn.tabs=function(d,b){var a=this.data("tabs");if(a){a.destroy();this.removeData("tabs")}if(c.isFunction(b))b=
+{onBeforeClick:b};b=c.extend({},c.tools.tabs.conf,b);this.each(function(){a=new p(c(this),d,b);c(this).data("tabs",a)});return b.api?a:this}})(jQuery);
+(function(f){function p(a,b,c){var h=c.relative?a.position().top:a.offset().top,d=c.relative?a.position().left:a.offset().left,i=c.position[0];h-=b.outerHeight()-c.offset[0];d+=a.outerWidth()+c.offset[1];if(/iPad/i.test(navigator.userAgent))h-=f(window).scrollTop();var j=b.outerHeight()+a.outerHeight();if(i=="center")h+=j/2;if(i=="bottom")h+=j;i=c.position[1];a=b.outerWidth()+a.outerWidth();if(i=="center")d-=a/2;if(i=="left")d-=a;return{top:h,left:d}}function u(a,b){var c=this,h=a.add(c),d,i=0,j=
+0,m=a.attr("title"),q=a.attr("data-tooltip"),r=o[b.effect],l,s=a.is(":input"),v=s&&a.is(":checkbox, :radio, select, :button, :submit"),t=a.attr("type"),k=b.events[t]||b.events[s?v?"widget":"input":"def"];if(!r)throw'Nonexistent effect "'+b.effect+'"';k=k.split(/,\s*/);if(k.length!=2)throw"Tooltip: bad events configuration for "+t;a.bind(k[0],function(e){clearTimeout(i);if(b.predelay)j=setTimeout(function(){c.show(e)},b.predelay);else c.show(e)}).bind(k[1],function(e){clearTimeout(j);if(b.delay)i=
+setTimeout(function(){c.hide(e)},b.delay);else c.hide(e)});if(m&&b.cancelDefault){a.removeAttr("title");a.data("title",m)}f.extend(c,{show:function(e){if(!d){if(q)d=f(q);else if(b.tip)d=f(b.tip).eq(0);else if(m)d=f(b.layout).addClass(b.tipClass).appendTo(document.body).hide().append(m);else{d=a.next();d.length||(d=a.parent().next())}if(!d.length)throw"Cannot find tooltip for "+a;}if(c.isShown())return c;d.stop(true,true);var g=p(a,d,b);b.tip&&d.html(a.data("title"));e=e||f.Event();e.type="onBeforeShow";
+h.trigger(e,[g]);if(e.isDefaultPrevented())return c;g=p(a,d,b);d.css({position:"absolute",top:g.top,left:g.left});l=true;r[0].call(c,function(){e.type="onShow";l="full";h.trigger(e)});g=b.events.tooltip.split(/,\s*/);if(!d.data("__set")){d.bind(g[0],function(){clearTimeout(i);clearTimeout(j)});g[1]&&!a.is("input:not(:checkbox, :radio), textarea")&&d.bind(g[1],function(n){n.relatedTarget!=a[0]&&a.trigger(k[1].split(" ")[0])});d.data("__set",true)}return c},hide:function(e){if(!d||!c.isShown())return c;
+e=e||f.Event();e.type="onBeforeHide";h.trigger(e);if(!e.isDefaultPrevented()){l=false;o[b.effect][1].call(c,function(){e.type="onHide";h.trigger(e)});return c}},isShown:function(e){return e?l=="full":l},getConf:function(){return b},getTip:function(){return d},getTrigger:function(){return a}});f.each("onHide,onBeforeShow,onShow,onBeforeHide".split(","),function(e,g){f.isFunction(b[g])&&f(c).bind(g,b[g]);c[g]=function(n){n&&f(c).bind(g,n);return c}})}f.tools=f.tools||{version:"1.2.5"};f.tools.tooltip=
+{conf:{effect:"toggle",fadeOutSpeed:"fast",predelay:0,delay:30,opacity:1,tip:0,position:["top","center"],offset:[0,0],relative:false,cancelDefault:true,events:{def:"mouseenter,mouseleave",input:"focus,blur",widget:"focus mouseenter,blur mouseleave",tooltip:"mouseenter,mouseleave"},layout:"<div/>",tipClass:"tooltip"},addEffect:function(a,b,c){o[a]=[b,c]}};var o={toggle:[function(a){var b=this.getConf(),c=this.getTip();b=b.opacity;b<1&&c.css({opacity:b});c.show();a.call()},function(a){this.getTip().hide();
+a.call()}],fade:[function(a){var b=this.getConf();this.getTip().fadeTo(b.fadeInSpeed,b.opacity,a)},function(a){this.getTip().fadeOut(this.getConf().fadeOutSpeed,a)}]};f.fn.tooltip=function(a){var b=this.data("tooltip");if(b)return b;a=f.extend(true,{},f.tools.tooltip.conf,a);if(typeof a.position=="string")a.position=a.position.split(/,?\s/);this.each(function(){b=new u(f(this),a);f(this).data("tooltip",b)});return a.api?b:this}})(jQuery);
+(function(a){function t(d,b){var c=this,j=d.add(c),o=a(window),k,f,m,g=a.tools.expose&&(b.mask||b.expose),n=Math.random().toString().slice(10);if(g){if(typeof g=="string")g={color:g};g.closeOnClick=g.closeOnEsc=false}var p=b.target||d.attr("rel");f=p?a(p):d;if(!f.length)throw"Could not find Overlay: "+p;d&&d.index(f)==-1&&d.click(function(e){c.load(e);return e.preventDefault()});a.extend(c,{load:function(e){if(c.isOpened())return c;var h=q[b.effect];if(!h)throw'Overlay: cannot find effect : "'+b.effect+
+'"';b.oneInstance&&a.each(s,function(){this.close(e)});e=e||a.Event();e.type="onBeforeLoad";j.trigger(e);if(e.isDefaultPrevented())return c;m=true;g&&a(f).expose(g);var i=b.top,r=b.left,u=f.outerWidth({margin:true}),v=f.outerHeight({margin:true});if(typeof i=="string")i=i=="center"?Math.max((o.height()-v)/2,0):parseInt(i,10)/100*o.height();if(r=="center")r=Math.max((o.width()-u)/2,0);h[0].call(c,{top:i,left:r},function(){if(m){e.type="onLoad";j.trigger(e)}});g&&b.closeOnClick&&a.mask.getMask().one("click",
+c.close);b.closeOnClick&&a(document).bind("click."+n,function(l){a(l.target).parents(f).length||c.close(l)});b.closeOnEsc&&a(document).bind("keydown."+n,function(l){l.keyCode==27&&c.close(l)});return c},close:function(e){if(!c.isOpened())return c;e=e||a.Event();e.type="onBeforeClose";j.trigger(e);if(!e.isDefaultPrevented()){m=false;q[b.effect][1].call(c,function(){e.type="onClose";j.trigger(e)});a(document).unbind("click."+n).unbind("keydown."+n);g&&a.mask.close();return c}},getOverlay:function(){return f},
+getTrigger:function(){return d},getClosers:function(){return k},isOpened:function(){return m},getConf:function(){return b}});a.each("onBeforeLoad,onStart,onLoad,onBeforeClose,onClose".split(","),function(e,h){a.isFunction(b[h])&&a(c).bind(h,b[h]);c[h]=function(i){i&&a(c).bind(h,i);return c}});k=f.find(b.close||".close");if(!k.length&&!b.close){k=a('<a class="close"></a>');f.prepend(k)}k.click(function(e){c.close(e)});b.load&&c.load()}a.tools=a.tools||{version:"1.2.5"};a.tools.overlay={addEffect:function(d,
+b,c){q[d]=[b,c]},conf:{close:null,closeOnClick:true,closeOnEsc:true,closeSpeed:"fast",effect:"default",fixed:!a.browser.msie||a.browser.version>6,left:"center",load:false,mask:null,oneInstance:true,speed:"normal",target:null,top:"10%"}};var s=[],q={};a.tools.overlay.addEffect("default",function(d,b){var c=this.getConf(),j=a(window);if(!c.fixed){d.top+=j.scrollTop();d.left+=j.scrollLeft()}d.position=c.fixed?"fixed":"absolute";this.getOverlay().css(d).fadeIn(c.speed,b)},function(d){this.getOverlay().fadeOut(this.getConf().closeSpeed,
+d)});a.fn.overlay=function(d){var b=this.data("overlay");if(b)return b;if(a.isFunction(d))d={onBeforeLoad:d};d=a.extend(true,{},a.tools.overlay.conf,d);this.each(function(){b=new t(a(this),d);s.push(b);a(this).data("overlay",b)});return d.api?b:this}})(jQuery);
+(function(e){function p(f,c){var b=e(c);return b.length<2?b:f.parent().find(c)}function u(f,c){var b=this,n=f.add(b),g=f.children(),l=0,j=c.vertical;k||(k=b);if(g.length>1)g=e(c.items,f);e.extend(b,{getConf:function(){return c},getIndex:function(){return l},getSize:function(){return b.getItems().size()},getNaviButtons:function(){return o.add(q)},getRoot:function(){return f},getItemWrap:function(){return g},getItems:function(){return g.children(c.item).not("."+c.clonedClass)},move:function(a,d){return b.seekTo(l+
+a,d)},next:function(a){return b.move(1,a)},prev:function(a){return b.move(-1,a)},begin:function(a){return b.seekTo(0,a)},end:function(a){return b.seekTo(b.getSize()-1,a)},focus:function(){return k=b},addItem:function(a){a=e(a);if(c.circular){g.children("."+c.clonedClass+":last").before(a);g.children("."+c.clonedClass+":first").replaceWith(a.clone().addClass(c.clonedClass))}else g.append(a);n.trigger("onAddItem",[a]);return b},seekTo:function(a,d,h){a.jquery||(a*=1);if(c.circular&&a===0&&l==-1&&d!==
+0)return b;if(!c.circular&&a<0||a>b.getSize()||a<-1)return b;var i=a;if(a.jquery)a=b.getItems().index(a);else i=b.getItems().eq(a);var r=e.Event("onBeforeSeek");if(!h){n.trigger(r,[a,d]);if(r.isDefaultPrevented()||!i.length)return b}i=j?{top:-i.position().top}:{left:-i.position().left};l=a;k=b;if(d===undefined)d=c.speed;g.animate(i,d,c.easing,h||function(){n.trigger("onSeek",[a])});return b}});e.each(["onBeforeSeek","onSeek","onAddItem"],function(a,d){e.isFunction(c[d])&&e(b).bind(d,c[d]);b[d]=function(h){h&&
+e(b).bind(d,h);return b}});if(c.circular){var s=b.getItems().slice(-1).clone().prependTo(g),t=b.getItems().eq(1).clone().appendTo(g);s.add(t).addClass(c.clonedClass);b.onBeforeSeek(function(a,d,h){if(!a.isDefaultPrevented())if(d==-1){b.seekTo(s,h,function(){b.end(0)});return a.preventDefault()}else d==b.getSize()&&b.seekTo(t,h,function(){b.begin(0)})});b.seekTo(0,0,function(){})}var o=p(f,c.prev).click(function(){b.prev()}),q=p(f,c.next).click(function(){b.next()});if(!c.circular&&b.getSize()>1){b.onBeforeSeek(function(a,
+d){setTimeout(function(){if(!a.isDefaultPrevented()){o.toggleClass(c.disabledClass,d<=0);q.toggleClass(c.disabledClass,d>=b.getSize()-1)}},1)});c.initialIndex||o.addClass(c.disabledClass)}c.mousewheel&&e.fn.mousewheel&&f.mousewheel(function(a,d){if(c.mousewheel){b.move(d<0?1:-1,c.wheelSpeed||50);return false}});if(c.touch){var m={};g[0].ontouchstart=function(a){a=a.touches[0];m.x=a.clientX;m.y=a.clientY};g[0].ontouchmove=function(a){if(a.touches.length==1&&!g.is(":animated")){var d=a.touches[0],h=
+m.x-d.clientX;d=m.y-d.clientY;b[j&&d>0||!j&&h>0?"next":"prev"]();a.preventDefault()}}}c.keyboard&&e(document).bind("keydown.scrollable",function(a){if(!(!c.keyboard||a.altKey||a.ctrlKey||e(a.target).is(":input")))if(!(c.keyboard!="static"&&k!=b)){var d=a.keyCode;if(j&&(d==38||d==40)){b.move(d==38?-1:1);return a.preventDefault()}if(!j&&(d==37||d==39)){b.move(d==37?-1:1);return a.preventDefault()}}});c.initialIndex&&b.seekTo(c.initialIndex,0,function(){})}e.tools=e.tools||{version:"1.2.5"};e.tools.scrollable=
+{conf:{activeClass:"active",circular:false,clonedClass:"cloned",disabledClass:"disabled",easing:"swing",initialIndex:0,item:null,items:".items",keyboard:true,mousewheel:false,next:".next",prev:".prev",speed:400,vertical:false,touch:true,wheelSpeed:0}};var k;e.fn.scrollable=function(f){var c=this.data("scrollable");if(c)return c;f=e.extend({},e.tools.scrollable.conf,f);this.each(function(){c=new u(e(this),f);e(this).data("scrollable",c)});return f.api?c:this}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jquery/jquery.treeTable.min.js b/azkaban-webserver/src/web/js/jquery/jquery.treeTable.min.js
new file mode 100644
index 0000000..c4bee9b
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/jquery.treeTable.min.js
@@ -0,0 +1,19 @@
+/*
+ * jQuery treeTable Plugin 2.3.0
+ * http://ludo.cubicphuse.nl/jquery-plugins/treeTable/
+ *
+ * Copyright 2010, Ludo van den Boom
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ */
+(function($){var options;var defaultPaddingLeft;$.fn.treeTable=function(opts){options=$.extend({},$.fn.treeTable.defaults,opts);return this.each(function(){$(this).addClass("treeTable").find("tbody tr").each(function(){if(!options.expandable||$(this)[0].className.search(options.childPrefix)==-1){if(isNaN(defaultPaddingLeft)){defaultPaddingLeft=parseInt($($(this).children("td")[options.treeColumn]).css('padding-left'),10);}
+initialize($(this));}else if(options.initialState=="collapsed"){this.style.display="none";}});});};$.fn.treeTable.defaults={childPrefix:"child-of-",clickableNodeNames:false,expandable:true,indent:19,initialState:"collapsed",treeColumn:0};$.fn.collapse=function(){$(this).addClass("collapsed");childrenOf($(this)).each(function(){if(!$(this).hasClass("collapsed")){$(this).collapse();}
+this.style.display="none";});return this;};$.fn.expand=function(){$(this).removeClass("collapsed").addClass("expanded");childrenOf($(this)).each(function(){initialize($(this));if($(this).is(".expanded.parent")){$(this).expand();}
+$(this).show();});return this;};$.fn.reveal=function(){$(ancestorsOf($(this)).reverse()).each(function(){initialize($(this));$(this).expand().show();});return this;};$.fn.appendBranchTo=function(destination){var node=$(this);var parent=parentOf(node);var ancestorNames=$.map(ancestorsOf($(destination)),function(a){return a.id;});if($.inArray(node[0].id,ancestorNames)==-1&&(!parent||(destination.id!=parent[0].id))&&destination.id!=node[0].id){indent(node,ancestorsOf(node).length*options.indent*-1);if(parent){node.removeClass(options.childPrefix+parent[0].id);}
+node.addClass(options.childPrefix+destination.id);move(node,destination);indent(node,ancestorsOf(node).length*options.indent);}
+return this;};$.fn.reverse=function(){return this.pushStack(this.get().reverse(),arguments);};$.fn.toggleBranch=function(){if($(this).hasClass("collapsed")){$(this).expand();}else{$(this).removeClass("expanded").collapse();}
+return this;};function ancestorsOf(node){var ancestors=[];while(node=parentOf(node)){ancestors[ancestors.length]=node[0];}
+return ancestors;};function childrenOf(node){return $("table.treeTable tbody tr."+options.childPrefix+node[0].id);};function getPaddingLeft(node){var paddingLeft=parseInt(node[0].style.paddingLeft,10);return(isNaN(paddingLeft))?defaultPaddingLeft:paddingLeft;}
+function indent(node,value){var cell=$(node.children("td")[options.treeColumn]);cell[0].style.paddingLeft=getPaddingLeft(cell)+value+"px";childrenOf(node).each(function(){indent($(this),value);});};function initialize(node){if(!node.hasClass("initialized")){node.addClass("initialized");var childNodes=childrenOf(node);if(!node.hasClass("parent")&&childNodes.length>0){node.addClass("parent");}
+if(node.hasClass("parent")){var cell=$(node.children("td")[options.treeColumn]);var padding=getPaddingLeft(cell)+options.indent;childNodes.each(function(){$(this).children("td")[options.treeColumn].style.paddingLeft=padding+"px";});if(options.expandable){cell.prepend('<span style="margin-left: -'+options.indent+'px; padding-left: '+options.indent+'px" class="expander"></span>');$(cell[0].firstChild).click(function(){node.toggleBranch();});if(options.clickableNodeNames){cell[0].style.cursor="pointer";$(cell).click(function(e){if(e.target.className!='expander'){node.toggleBranch();}});}
+if(!(node.hasClass("expanded")||node.hasClass("collapsed"))){node.addClass(options.initialState);}
+if(node.hasClass("expanded")){node.expand();}}}}};function move(node,destination){node.insertAfter(destination);childrenOf(node).reverse().each(function(){move($(this),node[0]);});};function parentOf(node){var classNames=node[0].className.split(' ');for(key in classNames){if(classNames[key].match(options.childPrefix)){return $("#"+classNames[key].substring(9));}}};})(jQuery);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/themes/apple/bg.jpg b/azkaban-webserver/src/web/js/jquery/themes/apple/bg.jpg
new file mode 100644
index 0000000..3aad05d
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/apple/bg.jpg differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/apple/d.png b/azkaban-webserver/src/web/js/jquery/themes/apple/d.png
new file mode 100644
index 0000000..2463ba6
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/apple/d.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/apple/dot_for_ie.gif b/azkaban-webserver/src/web/js/jquery/themes/apple/dot_for_ie.gif
new file mode 100644
index 0000000..c0cc5fd
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/apple/dot_for_ie.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/apple/style.css b/azkaban-webserver/src/web/js/jquery/themes/apple/style.css
new file mode 100644
index 0000000..8f1b3de
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/themes/apple/style.css
@@ -0,0 +1,60 @@
+/*
+ * jsTree apple theme 1.0
+ * Supported features: dots/no-dots, icons/no-icons, focused, loading
+ * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
+ */
+
+.jstree-apple > ul { background:url("bg.jpg") left top repeat; }
+.jstree-apple li,
+.jstree-apple ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
+.jstree-apple li { background-position:-90px 0; background-repeat:repeat-y; }
+.jstree-apple li.jstree-last { background:transparent; }
+.jstree-apple .jstree-open > ins { background-position:-72px 0; }
+.jstree-apple .jstree-closed > ins { background-position:-54px 0; }
+.jstree-apple .jstree-leaf > ins { background-position:-36px 0; }
+
+.jstree-apple a { border-radius:4px; -moz-border-radius:4px; -webkit-border-radius:4px; text-shadow:1px 1px 1px white; }
+.jstree-apple .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 3px 0 1px; text-shadow:1px 1px 1px silver; }
+.jstree-apple .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 3px 0 1px; }
+.jstree-apple a .jstree-icon { background-position:-56px -20px; }
+.jstree-apple a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }
+
+.jstree-apple.jstree-focused { background:white; }
+
+.jstree-apple .jstree-no-dots li,
+.jstree-apple .jstree-no-dots .jstree-leaf > ins { background:transparent; }
+.jstree-apple .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
+.jstree-apple .jstree-no-dots .jstree-closed > ins { background-position:0 0; }
+
+.jstree-apple .jstree-no-icons a .jstree-icon { display:none; }
+
+.jstree-apple .jstree-search { font-style:italic; }
+
+.jstree-apple .jstree-no-icons .jstree-checkbox { display:inline-block; }
+.jstree-apple .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
+.jstree-apple .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
+.jstree-apple .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
+.jstree-apple .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
+.jstree-apple .jstree-checked > a > .checkbox:hover { background-position:-38px -37px; }
+.jstree-apple .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
+.jstree-apple .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }
+
+#vakata-dragged.jstree-apple ins { background:transparent !important; }
+#vakata-dragged.jstree-apple .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
+#vakata-dragged.jstree-apple .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
+#jstree-marker.jstree-apple { background:url("d.png") -41px -57px no-repeat !important; }
+
+.jstree-apple a.jstree-search { color:aqua; }
+
+#vakata-contextmenu.jstree-apple-context,
+#vakata-contextmenu.jstree-apple-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
+#vakata-contextmenu.jstree-apple-context li { }
+#vakata-contextmenu.jstree-apple-context a { color:black; }
+#vakata-contextmenu.jstree-apple-context a:hover,
+#vakata-contextmenu.jstree-apple-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
+#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a,
+#vakata-contextmenu.jstree-apple-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
+#vakata-contextmenu.jstree-apple-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
+#vakata-contextmenu.jstree-apple-context li ul { margin-left:-4px; }
+
+/* TODO: IE6 support - the `>` selectors */
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/themes/apple/throbber.gif b/azkaban-webserver/src/web/js/jquery/themes/apple/throbber.gif
new file mode 100644
index 0000000..5b33f7e
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/apple/throbber.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/classic/d.png b/azkaban-webserver/src/web/js/jquery/themes/classic/d.png
new file mode 100644
index 0000000..275daec
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/classic/d.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/classic/dot_for_ie.gif b/azkaban-webserver/src/web/js/jquery/themes/classic/dot_for_ie.gif
new file mode 100644
index 0000000..c0cc5fd
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/classic/dot_for_ie.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/classic/style.css b/azkaban-webserver/src/web/js/jquery/themes/classic/style.css
new file mode 100644
index 0000000..bb15730
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/themes/classic/style.css
@@ -0,0 +1,59 @@
+/*
+ * jsTree classic theme 1.0
+ * Supported features: dots/no-dots, icons/no-icons, focused, loading
+ * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
+ */
+
+.jstree-classic li,
+.jstree-classic ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
+.jstree-classic li { background-position:-90px 0; background-repeat:repeat-y; }
+.jstree-classic li.jstree-last { background:transparent; }
+.jstree-classic .jstree-open > ins { background-position:-72px 0; }
+.jstree-classic .jstree-closed > ins { background-position:-54px 0; }
+.jstree-classic .jstree-leaf > ins { background-position:-36px 0; }
+
+.jstree-classic .jstree-hovered { background:#e7f4f9; border:1px solid #e7f4f9; padding:0 2px 0 1px; }
+.jstree-classic .jstree-clicked { background:navy; border:1px solid navy; padding:0 2px 0 1px; color:white; }
+.jstree-classic a .jstree-icon { background-position:-56px -19px; }
+.jstree-classic .jstree-open > a .jstree-icon { background-position:-56px -36px; }
+.jstree-classic a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }
+
+.jstree-classic.jstree-focused { background:white; }
+
+.jstree-classic .jstree-no-dots li,
+.jstree-classic .jstree-no-dots .jstree-leaf > ins { background:transparent; }
+.jstree-classic .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
+.jstree-classic .jstree-no-dots .jstree-closed > ins { background-position:0 0; }
+
+.jstree-classic .jstree-no-icons a .jstree-icon { display:none; }
+
+.jstree-classic .jstree-search { font-style:italic; }
+
+.jstree-classic .jstree-no-icons .jstree-checkbox { display:inline-block; }
+.jstree-classic .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
+.jstree-classic .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
+.jstree-classic .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
+.jstree-classic .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
+.jstree-classic .jstree-checked > a > .jstree-checkbox:hover { background-position:-38px -37px; }
+.jstree-classic .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
+.jstree-classic .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }
+
+#vakata-dragged.jstree-classic ins { background:transparent !important; }
+#vakata-dragged.jstree-classic .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
+#vakata-dragged.jstree-classic .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
+#jstree-marker.jstree-classic { background:url("d.png") -41px -57px no-repeat !important; }
+
+.jstree-classic a.jstree-search { color:aqua; }
+
+#vakata-contextmenu.jstree-classic-context,
+#vakata-contextmenu.jstree-classic-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
+#vakata-contextmenu.jstree-classic-context li { }
+#vakata-contextmenu.jstree-classic-context a { color:black; }
+#vakata-contextmenu.jstree-classic-context a:hover,
+#vakata-contextmenu.jstree-classic-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
+#vakata-contextmenu.jstree-classic-context li.jstree-contextmenu-disabled a,
+#vakata-contextmenu.jstree-classic-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
+#vakata-contextmenu.jstree-classic-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
+#vakata-contextmenu.jstree-classic-context li ul { margin-left:-4px; }
+
+/* TODO: IE6 support - the `>` selectors */
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/themes/classic/throbber.gif b/azkaban-webserver/src/web/js/jquery/themes/classic/throbber.gif
new file mode 100644
index 0000000..5b33f7e
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/classic/throbber.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default/d.gif b/azkaban-webserver/src/web/js/jquery/themes/default/d.gif
new file mode 100644
index 0000000..0e958d3
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/default/d.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default/d.png b/azkaban-webserver/src/web/js/jquery/themes/default/d.png
new file mode 100644
index 0000000..8540175
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/default/d.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default/style.css b/azkaban-webserver/src/web/js/jquery/themes/default/style.css
new file mode 100644
index 0000000..01a0889
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/themes/default/style.css
@@ -0,0 +1,73 @@
+/*
+ * jsTree default theme 1.0
+ * Supported features: dots/no-dots, icons/no-icons, focused, loading
+ * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
+ */
+
+.jstree-default li,
+.jstree-default ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
+.jstree-default li { background-position:-90px 0; background-repeat:repeat-y; }
+.jstree-default li.jstree-last { background:transparent; }
+.jstree-default .jstree-open > ins { background-position:-72px 0; }
+.jstree-default .jstree-closed > ins { background-position:-54px 0; }
+.jstree-default .jstree-leaf > ins { background-position:-36px 0; }
+
+.jstree-default .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 2px 0 1px; }
+.jstree-default .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 2px 0 1px; }
+.jstree-default a .jstree-icon { background-position:-56px -19px; }
+.jstree-default a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }
+
+.jstree-default.jstree-focused { background:#ffffee; }
+
+.jstree-default .jstree-no-dots li,
+.jstree-default .jstree-no-dots .jstree-leaf > ins { background:transparent; }
+.jstree-default .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
+.jstree-default .jstree-no-dots .jstree-closed > ins { background-position:0 0; }
+
+.jstree-default .jstree-no-icons a .jstree-icon { display:none; }
+
+.jstree-default .jstree-search { font-style:italic; }
+
+.jstree-default .jstree-no-icons .jstree-checkbox { display:inline-block; }
+.jstree-default .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
+.jstree-default .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
+.jstree-default .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
+.jstree-default .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
+.jstree-default .jstree-checked > a > .jstree-checkbox:hover { background-position:-38px -37px; }
+.jstree-default .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
+.jstree-default .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }
+
+#vakata-dragged.jstree-default ins { background:transparent !important; }
+#vakata-dragged.jstree-default .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
+#vakata-dragged.jstree-default .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
+#jstree-marker.jstree-default { background:url("d.png") -41px -57px no-repeat !important; }
+
+.jstree-default a.jstree-search { color:aqua; }
+
+#vakata-contextmenu.jstree-default-context,
+#vakata-contextmenu.jstree-default-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
+#vakata-contextmenu.jstree-default-context li { }
+#vakata-contextmenu.jstree-default-context a { color:black; }
+#vakata-contextmenu.jstree-default-context a:hover,
+#vakata-contextmenu.jstree-default-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
+#vakata-contextmenu.jstree-default-context li.jstree-contextmenu-disabled a,
+#vakata-contextmenu.jstree-default-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
+#vakata-contextmenu.jstree-default-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
+#vakata-contextmenu.jstree-default-context li ul { margin-left:-4px; }
+
+/* IE6 BEGIN */
+.jstree-default li,
+.jstree-default ins,
+#vakata-dragged.jstree-default .jstree-invalid,
+#vakata-dragged.jstree-default .jstree-ok,
+#jstree-marker.jstree-default { _background-image:url("d.gif"); }
+.jstree-default .jstree-open ins { _background-position:-72px 0; }
+.jstree-default .jstree-closed ins { _background-position:-54px 0; }
+.jstree-default .jstree-leaf ins { _background-position:-36px 0; }
+.jstree-default a ins.jstree-icon { _background-position:-56px -19px; }
+#vakata-contextmenu.jstree-default-context ins { _display:none; }
+#vakata-contextmenu.jstree-default-context li { _zoom:1; }
+.jstree-default .jstree-undetermined a .jstree-checkbox { _background-position:-20px -19px; }
+.jstree-default .jstree-checked a .jstree-checkbox { _background-position:-38px -19px; }
+.jstree-default .jstree-unchecked a .jstree-checkbox { _background-position:-2px -19px; }
+/* IE6 END */
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default/throbber.gif b/azkaban-webserver/src/web/js/jquery/themes/default/throbber.gif
new file mode 100644
index 0000000..5b33f7e
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/default/throbber.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default-rtl/d.gif b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/d.gif
new file mode 100644
index 0000000..d85aba0
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/d.gif differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default-rtl/d.png b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/d.png
new file mode 100644
index 0000000..5179cf6
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/d.png differ
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default-rtl/style.css b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/style.css
new file mode 100644
index 0000000..3ad0727
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/style.css
@@ -0,0 +1,83 @@
+/*
+ * jsTree default-rtl theme 1.0
+ * Supported features: dots/no-dots, icons/no-icons, focused, loading
+ * Supported plugins: ui (hovered, clicked), checkbox, contextmenu, search
+ */
+
+.jstree-default-rtl li,
+.jstree-default-rtl ins { background-image:url("d.png"); background-repeat:no-repeat; background-color:transparent; }
+.jstree-default-rtl li { background-position:-90px 0; background-repeat:repeat-y; }
+.jstree-default-rtl li.jstree-last { background:transparent; }
+.jstree-default-rtl .jstree-open > ins { background-position:-72px 0; }
+.jstree-default-rtl .jstree-closed > ins { background-position:-54px 0; }
+.jstree-default-rtl .jstree-leaf > ins { background-position:-36px 0; }
+
+.jstree-default-rtl .jstree-hovered { background:#e7f4f9; border:1px solid #d8f0fa; padding:0 2px 0 1px; }
+.jstree-default-rtl .jstree-clicked { background:#beebff; border:1px solid #99defd; padding:0 2px 0 1px; }
+.jstree-default-rtl a .jstree-icon { background-position:-56px -19px; }
+.jstree-default-rtl a.jstree-loading .jstree-icon { background:url("throbber.gif") center center no-repeat !important; }
+
+.jstree-default-rtl.jstree-focused { background:#ffffee; }
+
+.jstree-default-rtl .jstree-no-dots li,
+.jstree-default-rtl .jstree-no-dots .jstree-leaf > ins { background:transparent; }
+.jstree-default-rtl .jstree-no-dots .jstree-open > ins { background-position:-18px 0; }
+.jstree-default-rtl .jstree-no-dots .jstree-closed > ins { background-position:0 0; }
+
+.jstree-default-rtl .jstree-no-icons a .jstree-icon { display:none; }
+
+.jstree-default-rtl .jstree-search { font-style:italic; }
+
+.jstree-default-rtl .jstree-no-icons .jstree-checkbox { display:inline-block; }
+.jstree-default-rtl .jstree-no-checkboxes .jstree-checkbox { display:none !important; }
+.jstree-default-rtl .jstree-checked > a > .jstree-checkbox { background-position:-38px -19px; }
+.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox { background-position:-2px -19px; }
+.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox { background-position:-20px -19px; }
+.jstree-default-rtl .jstree-checked > a > .jstree-checkbox:hover { background-position:-38px -37px; }
+.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox:hover { background-position:-2px -37px; }
+.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-20px -37px; }
+
+#vakata-dragged.jstree-default-rtl ins { background:transparent !important; }
+#vakata-dragged.jstree-default-rtl .jstree-ok { background:url("d.png") -2px -53px no-repeat !important; }
+#vakata-dragged.jstree-default-rtl .jstree-invalid { background:url("d.png") -18px -53px no-repeat !important; }
+#jstree-marker.jstree-default-rtl { background:url("d.png") -41px -57px no-repeat !important; }
+
+.jstree-default-rtl a.jstree-search { color:aqua; }
+
+#vakata-contextmenu.jstree-default-rtl-context,
+#vakata-contextmenu.jstree-default-rtl-context li ul { background:#f0f0f0; border:1px solid #979797; -moz-box-shadow: 1px 1px 2px #999; -webkit-box-shadow: 1px 1px 2px #999; box-shadow: 1px 1px 2px #999; }
+#vakata-contextmenu.jstree-default-rtl-context li { }
+#vakata-contextmenu.jstree-default-rtl-context a { color:black; }
+#vakata-contextmenu.jstree-default-rtl-context a:hover,
+#vakata-contextmenu.jstree-default-rtl-context .vakata-hover > a { padding:0 5px; background:#e8eff7; border:1px solid #aecff7; color:black; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; }
+#vakata-contextmenu.jstree-default-rtl-context li.jstree-contextmenu-disabled a,
+#vakata-contextmenu.jstree-default-rtl-context li.jstree-contextmenu-disabled a:hover { color:silver; background:transparent; border:0; padding:1px 4px; }
+#vakata-contextmenu.jstree-default-rtl-context li.vakata-separator { background:white; border-top:1px solid #e0e0e0; margin:0; }
+#vakata-contextmenu.jstree-default-rtl-context li ul { margin-left:-4px; }
+
+/* IE6 BEGIN */
+.jstree-default-rtl li,
+.jstree-default-rtl ins,
+#vakata-dragged.jstree-default-rtl .jstree-invalid,
+#vakata-dragged.jstree-default-rtl .jstree-ok,
+#jstree-marker.jstree-default-rtl { _background-image:url("d.gif"); }
+.jstree-default-rtl .jstree-open ins { _background-position:-72px 0; }
+.jstree-default-rtl .jstree-closed ins { _background-position:-54px 0; }
+.jstree-default-rtl .jstree-leaf ins { _background-position:-36px 0; }
+.jstree-default-rtl a ins.jstree-icon { _background-position:-56px -19px; }
+#vakata-contextmenu.jstree-default-rtl-context ins { _display:none; }
+#vakata-contextmenu.jstree-default-rtl-context li { _zoom:1; }
+.jstree-default-rtl .jstree-undetermined a .jstree-checkbox { _background-position:-18px -19px; }
+.jstree-default-rtl .jstree-checked a .jstree-checkbox { _background-position:-36px -19px; }
+.jstree-default-rtl .jstree-unchecked a .jstree-checkbox { _background-position:0px -19px; }
+/* IE6 END */
+
+/* RTL part */
+.jstree-default-rtl .jstree-hovered, .jstree-default-rtl .jstree-clicked { padding:0 1px 0 2px; }
+.jstree-default-rtl li { background-image:url("dots.gif"); background-position: 100% 0px; }
+.jstree-default-rtl .jstree-checked > a > .jstree-checkbox { background-position:-36px -19px; margin-left:2px; }
+.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox { background-position:0px -19px; margin-left:2px; }
+.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox { background-position:-18px -19px; margin-left:2px; }
+.jstree-default-rtl .jstree-checked > a > .jstree-checkbox:hover { background-position:-36px -37px; }
+.jstree-default-rtl .jstree-unchecked > a > .jstree-checkbox:hover { background-position:0px -37px; }
+.jstree-default-rtl .jstree-undetermined > a > .jstree-checkbox:hover { background-position:-18px -37px; }
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/jquery/themes/default-rtl/throbber.gif b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/throbber.gif
new file mode 100644
index 0000000..5b33f7e
Binary files /dev/null and b/azkaban-webserver/src/web/js/jquery/themes/default-rtl/throbber.gif differ
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.blind.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.blind.min.js
new file mode 100644
index 0000000..d06c958
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.blind.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Blind 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Blind
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(b){b.effects.blind=function(c){return this.queue(function(){var a=b(this),g=["position","top","left"],f=b.effects.setMode(a,c.options.mode||"hide"),d=c.options.direction||"vertical";b.effects.save(a,g);a.show();var e=b.effects.createWrapper(a).css({overflow:"hidden"}),h=d=="vertical"?"height":"width";d=d=="vertical"?e.height():e.width();f=="show"&&e.css(h,0);var i={};i[h]=f=="show"?d:0;e.animate(i,c.duration,c.options.easing,function(){f=="hide"&&a.hide();b.effects.restore(a,g);b.effects.removeWrapper(a);
+c.callback&&c.callback.apply(a[0],arguments);a.dequeue()})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.bounce.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.bounce.min.js
new file mode 100644
index 0000000..2faff20
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.bounce.min.js
@@ -0,0 +1,15 @@
+/*
+ * jQuery UI Effects Bounce 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Bounce
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(e){e.effects.bounce=function(b){return this.queue(function(){var a=e(this),l=["position","top","left"],h=e.effects.setMode(a,b.options.mode||"effect"),d=b.options.direction||"up",c=b.options.distance||20,m=b.options.times||5,i=b.duration||250;/show|hide/.test(h)&&l.push("opacity");e.effects.save(a,l);a.show();e.effects.createWrapper(a);var f=d=="up"||d=="down"?"top":"left";d=d=="up"||d=="left"?"pos":"neg";c=b.options.distance||(f=="top"?a.outerHeight({margin:true})/3:a.outerWidth({margin:true})/
+3);if(h=="show")a.css("opacity",0).css(f,d=="pos"?-c:c);if(h=="hide")c/=m*2;h!="hide"&&m--;if(h=="show"){var g={opacity:1};g[f]=(d=="pos"?"+=":"-=")+c;a.animate(g,i/2,b.options.easing);c/=2;m--}for(g=0;g<m;g++){var j={},k={};j[f]=(d=="pos"?"-=":"+=")+c;k[f]=(d=="pos"?"+=":"-=")+c;a.animate(j,i/2,b.options.easing).animate(k,i/2,b.options.easing);c=h=="hide"?c*2:c/2}if(h=="hide"){g={opacity:0};g[f]=(d=="pos"?"-=":"+=")+c;a.animate(g,i/2,b.options.easing,function(){a.hide();e.effects.restore(a,l);e.effects.removeWrapper(a);
+b.callback&&b.callback.apply(this,arguments)})}else{j={};k={};j[f]=(d=="pos"?"-=":"+=")+c;k[f]=(d=="pos"?"+=":"-=")+c;a.animate(j,i/2,b.options.easing).animate(k,i/2,b.options.easing,function(){e.effects.restore(a,l);e.effects.removeWrapper(a);b.callback&&b.callback.apply(this,arguments)})}a.queue("fx",function(){a.dequeue()});a.dequeue()})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.clip.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.clip.min.js
new file mode 100644
index 0000000..814e454
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.clip.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Clip 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Clip
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(b){b.effects.clip=function(e){return this.queue(function(){var a=b(this),i=["position","top","left","height","width"],f=b.effects.setMode(a,e.options.mode||"hide"),c=e.options.direction||"vertical";b.effects.save(a,i);a.show();var d=b.effects.createWrapper(a).css({overflow:"hidden"});d=a[0].tagName=="IMG"?d:a;var g={size:c=="vertical"?"height":"width",position:c=="vertical"?"top":"left"};c=c=="vertical"?d.height():d.width();if(f=="show"){d.css(g.size,0);d.css(g.position,c/2)}var h={};h[g.size]=
+f=="show"?c:0;h[g.position]=f=="show"?0:c/2;d.animate(h,{queue:false,duration:e.duration,easing:e.options.easing,complete:function(){f=="hide"&&a.hide();b.effects.restore(a,i);b.effects.removeWrapper(a);e.callback&&e.callback.apply(a[0],arguments);a.dequeue()}})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.core.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.core.min.js
new file mode 100644
index 0000000..585751d
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.core.min.js
@@ -0,0 +1,30 @@
+/*
+ * jQuery UI Effects 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/
+ */
+jQuery.effects||function(f,j){function l(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1],
+16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return m.transparent;return m[f.trim(c).toLowerCase()]}function r(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return l(b)}function n(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,
+a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function o(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in s||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function t(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d=
+a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:f.fx.speeds[b]||f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=r(b.elem,a);b.end=l(b.end);b.colorInit=
+true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var m={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,
+183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,
+165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},p=["add","remove","toggle"],s={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b,d){if(f.isFunction(b)){d=b;b=null}return this.each(function(){var e=f(this),g=e.attr("style")||" ",h=o(n.call(this)),q,u=e.attr("className");f.each(p,function(v,
+i){c[i]&&e[i+"Class"](c[i])});q=o(n.call(this));e.attr("className",u);e.animate(t(h,q),a,b,function(){f.each(p,function(v,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments)})})};f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?
+f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this,[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.5",save:function(c,a){for(var b=0;b<a.length;b++)a[b]!==
+null&&c.data("ec.storage."+a[b],c[0].style[a[b]])},restore:function(c,a){for(var b=0;b<a.length;b++)a[b]!==null&&c.css(a[b],c.data("ec.storage."+a[b]))},setMode:function(c,a){if(a=="toggle")a=c.is(":hidden")?"show":"hide";return a},getBaseline:function(c,a){var b;switch(c[0]){case "top":b=0;break;case "middle":b=0.5;break;case "bottom":b=1;break;default:b=c[0]/a.height}switch(c[1]){case "left":c=0;break;case "center":c=0.5;break;case "right":c=1;break;default:c=c[1]/a.width}return{x:c,y:b}},createWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent();
+var a={width:c.outerWidth(true),height:c.outerHeight(true),"float":c.css("float")},b=f("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"});
+c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c||
+typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c]||typeof c==
+"boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,
+a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=
+e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+
+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/
+2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h<Math.abs(d)){h=d;c=g/4}else c=g/(2*Math.PI)*Math.asin(d/h);return-(h*Math.pow(2,10*(a-=1))*Math.sin((a*e-c)*2*Math.PI/g))+b},easeOutElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h<Math.abs(d)){h=d;c=g/4}else c=g/(2*Math.PI)*Math.asin(d/h);return h*Math.pow(2,-10*
+a)*Math.sin((a*e-c)*2*Math.PI/g)+d+b},easeInOutElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e/2)==2)return b+d;g||(g=e*0.3*1.5);if(h<Math.abs(d)){h=d;c=g/4}else c=g/(2*Math.PI)*Math.asin(d/h);if(a<1)return-0.5*h*Math.pow(2,10*(a-=1))*Math.sin((a*e-c)*2*Math.PI/g)+b;return h*Math.pow(2,-10*(a-=1))*Math.sin((a*e-c)*2*Math.PI/g)*0.5+d+b},easeInBack:function(c,a,b,d,e,g){if(g==j)g=1.70158;return d*(a/=e)*a*((g+1)*a-g)+b},easeOutBack:function(c,a,b,d,e,g){if(g==j)g=1.70158;
+return d*((a=a/e-1)*a*((g+1)*a+g)+1)+b},easeInOutBack:function(c,a,b,d,e,g){if(g==j)g=1.70158;if((a/=e/2)<1)return d/2*a*a*(((g*=1.525)+1)*a-g)+b;return d/2*((a-=2)*a*(((g*=1.525)+1)*a+g)+2)+b},easeInBounce:function(c,a,b,d,e){return d-f.easing.easeOutBounce(c,e-a,0,d,e)+b},easeOutBounce:function(c,a,b,d,e){return(a/=e)<1/2.75?d*7.5625*a*a+b:a<2/2.75?d*(7.5625*(a-=1.5/2.75)*a+0.75)+b:a<2.5/2.75?d*(7.5625*(a-=2.25/2.75)*a+0.9375)+b:d*(7.5625*(a-=2.625/2.75)*a+0.984375)+b},easeInOutBounce:function(c,
+a,b,d,e){if(a<e/2)return f.easing.easeInBounce(c,a*2,0,d,e)*0.5+b;return f.easing.easeOutBounce(c,a*2-e,0,d,e)*0.5+d*0.5+b}})}(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.drop.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.drop.min.js
new file mode 100644
index 0000000..5ffed43
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.drop.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Drop 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Drop
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(c){c.effects.drop=function(d){return this.queue(function(){var a=c(this),h=["position","top","left","opacity"],e=c.effects.setMode(a,d.options.mode||"hide"),b=d.options.direction||"left";c.effects.save(a,h);a.show();c.effects.createWrapper(a);var f=b=="up"||b=="down"?"top":"left";b=b=="up"||b=="left"?"pos":"neg";var g=d.options.distance||(f=="top"?a.outerHeight({margin:true})/2:a.outerWidth({margin:true})/2);if(e=="show")a.css("opacity",0).css(f,b=="pos"?-g:g);var i={opacity:e=="show"?1:
+0};i[f]=(e=="show"?b=="pos"?"+=":"-=":b=="pos"?"-=":"+=")+g;a.animate(i,{queue:false,duration:d.duration,easing:d.options.easing,complete:function(){e=="hide"&&a.hide();c.effects.restore(a,h);c.effects.removeWrapper(a);d.callback&&d.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.explode.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.explode.min.js
new file mode 100644
index 0000000..4dff59f
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.explode.min.js
@@ -0,0 +1,15 @@
+/*
+ * jQuery UI Effects Explode 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Explode
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(j){j.effects.explode=function(a){return this.queue(function(){var c=a.options.pieces?Math.round(Math.sqrt(a.options.pieces)):3,d=a.options.pieces?Math.round(Math.sqrt(a.options.pieces)):3;a.options.mode=a.options.mode=="toggle"?j(this).is(":visible")?"hide":"show":a.options.mode;var b=j(this).show().css("visibility","hidden"),g=b.offset();g.top-=parseInt(b.css("marginTop"),10)||0;g.left-=parseInt(b.css("marginLeft"),10)||0;for(var h=b.outerWidth(true),i=b.outerHeight(true),e=0;e<c;e++)for(var f=
+0;f<d;f++)b.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+
+e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.fade.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.fade.min.js
new file mode 100644
index 0000000..f47883d
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.fade.min.js
@@ -0,0 +1,13 @@
+/*
+ * jQuery UI Effects Fade 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Fade
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.fold.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.fold.min.js
new file mode 100644
index 0000000..fe762c8
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.fold.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Fold 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Fold
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","left"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100*
+f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.highlight.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.highlight.min.js
new file mode 100644
index 0000000..7327660
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.highlight.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Highlight 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Highlight
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&&
+this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.pulsate.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.pulsate.min.js
new file mode 100644
index 0000000..93e2942
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.pulsate.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Pulsate 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Pulsate
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c<times;c++){b.animate({opacity:animateTo},duration,a.options.easing);animateTo=(animateTo+1)%2}b.animate({opacity:animateTo},duration,
+a.options.easing,function(){animateTo==0&&b.hide();a.callback&&a.callback.apply(this,arguments)});b.queue("fx",function(){b.dequeue()}).dequeue()})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.scale.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.scale.min.js
new file mode 100644
index 0000000..3603e77
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.scale.min.js
@@ -0,0 +1,20 @@
+/*
+ * jQuery UI Effects Scale 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Scale
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(c){c.effects.puff=function(b){return this.queue(function(){var a=c(this),e=c.effects.setMode(a,b.options.mode||"hide"),g=parseInt(b.options.percent,10)||150,h=g/100,i={height:a.height(),width:a.width()};c.extend(b.options,{fade:true,mode:e,percent:e=="hide"?g:100,from:e=="hide"?i:{height:i.height*h,width:i.width*h}});a.effect("scale",b.options,b.duration,b.callback);a.dequeue()})};c.effects.scale=function(b){return this.queue(function(){var a=c(this),e=c.extend(true,{},b.options),g=c.effects.setMode(a,
+b.options.mode||"effect"),h=parseInt(b.options.percent,10)||(parseInt(b.options.percent,10)==0?0:g=="hide"?0:100),i=b.options.direction||"both",f=b.options.origin;if(g!="effect"){e.origin=f||["middle","center"];e.restore=true}f={height:a.height(),width:a.width()};a.from=b.options.from||(g=="show"?{height:0,width:0}:f);h={y:i!="horizontal"?h/100:1,x:i!="vertical"?h/100:1};a.to={height:f.height*h.y,width:f.width*h.x};if(b.options.fade){if(g=="show"){a.from.opacity=0;a.to.opacity=1}if(g=="hide"){a.from.opacity=
+1;a.to.opacity=0}}e.from=a.from;e.to=a.to;e.mode=g;a.effect("size",e,b.duration,b.callback);a.dequeue()})};c.effects.size=function(b){return this.queue(function(){var a=c(this),e=["position","top","left","width","height","overflow","opacity"],g=["position","top","left","overflow","opacity"],h=["width","height","overflow"],i=["fontSize"],f=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],k=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=c.effects.setMode(a,
+b.options.mode||"effect"),n=b.options.restore||false,m=b.options.scale||"both",l=b.options.origin,j={height:a.height(),width:a.width()};a.from=b.options.from||j;a.to=b.options.to||j;if(l){l=c.effects.getBaseline(l,j);a.from.top=(j.height-a.from.height)*l.y;a.from.left=(j.width-a.from.width)*l.x;a.to.top=(j.height-a.to.height)*l.y;a.to.left=(j.width-a.to.width)*l.x}var d={from:{y:a.from.height/j.height,x:a.from.width/j.width},to:{y:a.to.height/j.height,x:a.to.width/j.width}};if(m=="box"||m=="both"){if(d.from.y!=
+d.to.y){e=e.concat(f);a.from=c.effects.setTransition(a,f,d.from.y,a.from);a.to=c.effects.setTransition(a,f,d.to.y,a.to)}if(d.from.x!=d.to.x){e=e.concat(k);a.from=c.effects.setTransition(a,k,d.from.x,a.from);a.to=c.effects.setTransition(a,k,d.to.x,a.to)}}if(m=="content"||m=="both")if(d.from.y!=d.to.y){e=e.concat(i);a.from=c.effects.setTransition(a,i,d.from.y,a.from);a.to=c.effects.setTransition(a,i,d.to.y,a.to)}c.effects.save(a,n?e:g);a.show();c.effects.createWrapper(a);a.css("overflow","hidden").css(a.from);
+if(m=="content"||m=="both"){f=f.concat(["marginTop","marginBottom"]).concat(i);k=k.concat(["marginLeft","marginRight"]);h=e.concat(f).concat(k);a.find("*[width]").each(function(){child=c(this);n&&c.effects.save(child,h);var o={height:child.height(),width:child.width()};child.from={height:o.height*d.from.y,width:o.width*d.from.x};child.to={height:o.height*d.to.y,width:o.width*d.to.x};if(d.from.y!=d.to.y){child.from=c.effects.setTransition(child,f,d.from.y,child.from);child.to=c.effects.setTransition(child,
+f,d.to.y,child.to)}if(d.from.x!=d.to.x){child.from=c.effects.setTransition(child,k,d.from.x,child.from);child.to=c.effects.setTransition(child,k,d.to.x,child.to)}child.css(child.from);child.animate(child.to,b.duration,b.options.easing,function(){n&&c.effects.restore(child,h)})})}a.animate(a.to,{queue:false,duration:b.duration,easing:b.options.easing,complete:function(){a.to.opacity===0&&a.css("opacity",a.from.opacity);p=="hide"&&a.hide();c.effects.restore(a,n?e:g);c.effects.removeWrapper(a);b.callback&&
+b.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.shake.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.shake.min.js
new file mode 100644
index 0000000..9849b05
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.shake.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Shake 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Shake
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(d){d.effects.shake=function(a){return this.queue(function(){var b=d(this),j=["position","top","left"];d.effects.setMode(b,a.options.mode||"effect");var c=a.options.direction||"left",e=a.options.distance||20,l=a.options.times||3,f=a.duration||a.options.duration||140;d.effects.save(b,j);b.show();d.effects.createWrapper(b);var g=c=="up"||c=="down"?"top":"left",h=c=="up"||c=="left"?"pos":"neg";c={};var i={},k={};c[g]=(h=="pos"?"-=":"+=")+e;i[g]=(h=="pos"?"+=":"-=")+e*2;k[g]=(h=="pos"?"-=":"+=")+
+e*2;b.animate(c,f,a.options.easing);for(e=1;e<l;e++)b.animate(i,f,a.options.easing).animate(k,f,a.options.easing);b.animate(i,f,a.options.easing).animate(c,f/2,a.options.easing,function(){d.effects.restore(b,j);d.effects.removeWrapper(b);a.callback&&a.callback.apply(this,arguments)});b.queue("fx",function(){b.dequeue()});b.dequeue()})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.slide.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.slide.min.js
new file mode 100644
index 0000000..950067a
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.slide.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Slide 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Slide
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(c){c.effects.slide=function(d){return this.queue(function(){var a=c(this),h=["position","top","left"],e=c.effects.setMode(a,d.options.mode||"show"),b=d.options.direction||"left";c.effects.save(a,h);a.show();c.effects.createWrapper(a).css({overflow:"hidden"});var f=b=="up"||b=="down"?"top":"left";b=b=="up"||b=="left"?"pos":"neg";var g=d.options.distance||(f=="top"?a.outerHeight({margin:true}):a.outerWidth({margin:true}));if(e=="show")a.css(f,b=="pos"?-g:g);var i={};i[f]=(e=="show"?b=="pos"?
+"+=":"-=":b=="pos"?"-=":"+=")+g;a.animate(i,{queue:false,duration:d.duration,easing:d.options.easing,complete:function(){e=="hide"&&a.hide();c.effects.restore(a,h);c.effects.removeWrapper(a);d.callback&&d.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.effects.transfer.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.transfer.min.js
new file mode 100644
index 0000000..3032416
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.effects.transfer.min.js
@@ -0,0 +1,14 @@
+/*
+ * jQuery UI Effects Transfer 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Transfer
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */
+(function(e){e.effects.transfer=function(a){return this.queue(function(){var b=e(this),c=e(a.options.to),d=c.offset();c={top:d.top,left:d.left,height:c.innerHeight(),width:c.innerWidth()};d=b.offset();var f=e('<div class="ui-effects-transfer"></div>').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments);
+b.dequeue()})})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.accordion.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.accordion.min.js
new file mode 100644
index 0000000..0579036
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.accordion.min.js
@@ -0,0 +1,30 @@
+/*
+ * jQuery UI Accordion 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function(c){c.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:true,clearStyle:false,collapsible:false,event:"click",fillSpace:false,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix");
+a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");
+if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var f=d.closest(".ui-accordion-header");a.active=f.length?f:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion",function(g){return a._keydown(g)}).next().attr("role",
+"tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(g){a._clickHandler.call(a,g,this);g.preventDefault()})},_createIcons:function(){var a=this.options;if(a.icons){c("<span></span>").addClass("ui-icon "+a.icons.header).prependTo(this.headers);
+this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("tabIndex");
+this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons();
+b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,f=this.headers.index(a.target),g=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:g=this.headers[(f+1)%d];break;case b.LEFT:case b.UP:g=this.headers[(f-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target);
+a.preventDefault()}if(g){c(a.target).attr("tabIndex",-1);c(g).attr("tabIndex",0);g.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+
+c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options;
+if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected);
+a.next().addClass("ui-accordion-content-active")}h=a.next();f=this.active.next();g={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):h,oldContent:f};d=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(h,f,g,b,d)}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);
+this.active.next().addClass("ui-accordion-content-active");var f=this.active.next(),g={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:f},h=this.active=c([]);this._toggle(h,f,g)}},_toggle:function(a,b,d,f,g){var h=this,e=h.options;h.toShow=a;h.toHide=b;h.data=d;var j=function(){if(h)return h._completed.apply(h,arguments)};h._trigger("changestart",null,h.data);h.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&f?{toShow:c([]),toHide:b,complete:j,
+down:g,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:g,autoHeight:e.autoHeight||e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;f=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!f[k]&&!c.easing[k])k="slide";f[k]||(f[k]=function(l){this.slide(l,{easing:k,duration:i||700})});
+f[k](d)}else{if(e.collapsible&&f)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.5",animations:{slide:function(a,
+b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),f=0,g={},h={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){h[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/);g[i]={value:j[1],
+unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(h,{step:function(j,i){if(i.prop=="height")f=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=f*g[i.prop].value+g[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide",paddingTop:"hide",
+paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.autocomplete.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.autocomplete.min.js
new file mode 100644
index 0000000..e0f3bb7
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.autocomplete.min.js
@@ -0,0 +1,31 @@
+/*
+ * jQuery UI Autocomplete 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ * jquery.ui.position.js
+ */
+(function(e){e.widget("ui.autocomplete",{options:{appendTo:"body",delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},_create:function(){var a=this,b=this.element[0].ownerDocument;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!a.options.disabled){var d=e.ui.keyCode;switch(c.keyCode){case d.PAGE_UP:a._move("previousPage",
+c);break;case d.PAGE_DOWN:a._move("nextPage",c);break;case d.UP:a._move("previous",c);c.preventDefault();break;case d.DOWN:a._move("next",c);c.preventDefault();break;case d.ENTER:case d.NUMPAD_ENTER:a.menu.element.is(":visible")&&c.preventDefault();case d.TAB:if(!a.menu.active)return;a.menu.select(c);break;case d.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);
+break}}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};this.menu=e("<ul></ul>").addClass("ui-autocomplete").appendTo(e(this.options.appendTo||"body",b)[0]).mousedown(function(c){var d=a.menu.element[0];
+c.target===d&&setTimeout(function(){e(document).one("mousedown",function(f){f.target!==a.element[0]&&f.target!==d&&!e.ui.contains(d,f.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,d){d=d.item.data("item.autocomplete");false!==a._trigger("focus",null,{item:d})&&/^key/.test(c.originalEvent.type)&&a.element.val(d.value)},selected:function(c,d){d=d.item.data("item.autocomplete");var f=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();
+a.previous=f}if(false!==a._trigger("select",c,{item:d})){a.term=d.value;a.element.val(d.value)}a.close(c);a.selectedItem=d},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");e.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");
+this.menu.element.remove();e.Widget.prototype.destroy.call(this)},_setOption:function(a,b){e.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(e(b||"body",this.element[0].ownerDocument)[0])},_initSource:function(){var a=this,b,c;if(e.isArray(this.options.source)){b=this.options.source;this.source=function(d,f){f(e.ui.autocomplete.filter(b,d.term))}}else if(typeof this.options.source==="string"){c=this.options.source;this.source=
+function(d,f){a.xhr&&a.xhr.abort();a.xhr=e.getJSON(c,d,function(g,i,h){h===a.xhr&&f(g);a.xhr=null})}}else this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search")!==false)return this._search(a)},_search:function(a){this.element.addClass("ui-autocomplete-loading");this.source({term:a},this.response)},_response:function(a){if(a.length){a=
+this._normalize(a);this._suggest(a);this._trigger("open")}else this.close();this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this._trigger("close",a);this.menu.element.hide();this.menu.deactivate()}},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(a){if(a.length&&a[0].label&&a[0].value)return a;return e.map(a,function(b){if(typeof b===
+"string")return{label:b,value:b};return e.extend({label:b.label||b.value,value:b.value||b.label},b)})},_suggest:function(a){var b=this.menu.element.empty().zIndex(this.element.zIndex()+1),c;this._renderMenu(b,a);this.menu.deactivate();this.menu.refresh();this.menu.element.show().position(e.extend({of:this.element},this.options.position));a=b.width("").outerWidth();c=this.element.outerWidth();b.outerWidth(Math.max(a,c))},_renderMenu:function(a,b){var c=this;e.each(b,function(d,f){c._renderItem(a,f)})},
+_renderItem:function(a,b){return e("<li></li>").data("item.autocomplete",b).append(e("<a></a>").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});e.extend(e.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},
+filter:function(a,b){var c=new RegExp(e.ui.autocomplete.escapeRegex(b),"i");return e.grep(a,function(d){return c.test(d.label||d.value||d)})}})})(jQuery);
+(function(e){e.widget("ui.menu",{_create:function(){var a=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(b){if(e(b.target).closest(".ui-menu-item a").length){b.preventDefault();a.select(b)}});this.refresh()},refresh:function(){var a=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex",
+-1).mouseenter(function(b){a.activate(b,e(this).parent())}).mouseleave(function(){a.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.attr("scrollTop"),f=this.element.height();if(c<0)this.element.attr("scrollTop",d+c);else c>=f&&this.element.attr("scrollTop",d+c-f+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",a,{item:b})},
+deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");this._trigger("blur");this.active=null}},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(this.active){a=this.active[a+"All"](".ui-menu-item").eq(0);
+a.length?this.activate(c,a):this.activate(c,this.element.children(b))}else this.activate(c,this.element.children(b))},nextPage:function(a){if(this.hasScroll())if(!this.active||this.last())this.activate(a,this.element.children(":first"));else{var b=this.active.offset().top,c=this.element.height(),d=this.element.children("li").filter(function(){var f=e(this).offset().top-b-c+e(this).height();return f<10&&f>-10});d.length||(d=this.element.children(":last"));this.activate(a,d)}else this.activate(a,this.element.children(!this.active||
+this.last()?":first":":last"))},previousPage:function(a){if(this.hasScroll())if(!this.active||this.first())this.activate(a,this.element.children(":last"));else{var b=this.active.offset().top,c=this.element.height();result=this.element.children("li").filter(function(){var d=e(this).offset().top-b+c-e(this).height();return d<10&&d>-10});result.length||(result=this.element.children(":first"));this.activate(a,result)}else this.activate(a,this.element.children(!this.active||this.first()?":last":":first"))},
+hasScroll:function(){return this.element.height()<this.element.attr("scrollHeight")},select:function(a){this._trigger("selected",a,{item:this.active})}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.button.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.button.min.js
new file mode 100644
index 0000000..1b974b2
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.button.min.js
@@ -0,0 +1,25 @@
+/*
+ * jQuery UI Button 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function(a){var g,i=function(b){a(":ui-button",b.target.form).each(function(){var c=a(this).data("button");setTimeout(function(){c.refresh()},1)})},h=function(b){var c=b.name,d=b.form,e=a([]);if(c)e=d?a(d).find("[name='"+c+"']"):a("[name='"+c+"']",b.ownerDocument).filter(function(){return!this.form});return e};a.widget("ui.button",{options:{disabled:null,text:true,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset.button").bind("reset.button",
+i);if(typeof this.options.disabled!=="boolean")this.options.disabled=this.element.attr("disabled");this._determineButtonType();this.hasTitle=!!this.buttonElement.attr("title");var b=this,c=this.options,d=this.type==="checkbox"||this.type==="radio",e="ui-state-hover"+(!d?" ui-state-active":"");if(c.label===null)c.label=this.buttonElement.html();if(this.element.is(":disabled"))c.disabled=true;this.buttonElement.addClass("ui-button ui-widget ui-state-default ui-corner-all").attr("role","button").bind("mouseenter.button",
+function(){if(!c.disabled){a(this).addClass("ui-state-hover");this===g&&a(this).addClass("ui-state-active")}}).bind("mouseleave.button",function(){c.disabled||a(this).removeClass(e)}).bind("focus.button",function(){a(this).addClass("ui-state-focus")}).bind("blur.button",function(){a(this).removeClass("ui-state-focus")});d&&this.element.bind("change.button",function(){b.refresh()});if(this.type==="checkbox")this.buttonElement.bind("click.button",function(){if(c.disabled)return false;a(this).toggleClass("ui-state-active");
+b.buttonElement.attr("aria-pressed",b.element[0].checked)});else if(this.type==="radio")this.buttonElement.bind("click.button",function(){if(c.disabled)return false;a(this).addClass("ui-state-active");b.buttonElement.attr("aria-pressed",true);var f=b.element[0];h(f).not(f).map(function(){return a(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed",false)});else{this.buttonElement.bind("mousedown.button",function(){if(c.disabled)return false;a(this).addClass("ui-state-active");
+g=this;a(document).one("mouseup",function(){g=null})}).bind("mouseup.button",function(){if(c.disabled)return false;a(this).removeClass("ui-state-active")}).bind("keydown.button",function(f){if(c.disabled)return false;if(f.keyCode==a.ui.keyCode.SPACE||f.keyCode==a.ui.keyCode.ENTER)a(this).addClass("ui-state-active")}).bind("keyup.button",function(){a(this).removeClass("ui-state-active")});this.buttonElement.is("a")&&this.buttonElement.keyup(function(f){f.keyCode===a.ui.keyCode.SPACE&&a(this).click()})}this._setOption("disabled",
+c.disabled)},_determineButtonType:function(){this.type=this.element.is(":checkbox")?"checkbox":this.element.is(":radio")?"radio":this.element.is("input")?"input":"button";if(this.type==="checkbox"||this.type==="radio"){this.buttonElement=this.element.parents().last().find("label[for="+this.element.attr("id")+"]");this.element.addClass("ui-helper-hidden-accessible");var b=this.element.is(":checked");b&&this.buttonElement.addClass("ui-state-active");this.buttonElement.attr("aria-pressed",b)}else this.buttonElement=
+this.element},widget:function(){return this.buttonElement},destroy:function(){this.element.removeClass("ui-helper-hidden-accessible");this.buttonElement.removeClass("ui-button ui-widget ui-state-default ui-corner-all ui-state-hover ui-state-active ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only").removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html());this.hasTitle||
+this.buttonElement.removeAttr("title");a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments);if(b==="disabled")c?this.element.attr("disabled",true):this.element.removeAttr("disabled");this._resetButton()},refresh:function(){var b=this.element.is(":disabled");b!==this.options.disabled&&this._setOption("disabled",b);if(this.type==="radio")h(this.element[0]).each(function(){a(this).is(":checked")?a(this).button("widget").addClass("ui-state-active").attr("aria-pressed",
+true):a(this).button("widget").removeClass("ui-state-active").attr("aria-pressed",false)});else if(this.type==="checkbox")this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed",true):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed",false)},_resetButton:function(){if(this.type==="input")this.options.label&&this.element.val(this.options.label);else{var b=this.buttonElement.removeClass("ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only"),
+c=a("<span></span>").addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary;if(d.primary||d.secondary){b.addClass("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary"));d.primary&&b.prepend("<span class='ui-button-icon-primary ui-icon "+d.primary+"'></span>");d.secondary&&b.append("<span class='ui-button-icon-secondary ui-icon "+d.secondary+"'></span>");if(!this.options.text){b.addClass(e?"ui-button-icons-only":"ui-button-icon-only").removeClass("ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary");
+this.hasTitle||b.attr("title",c)}}else b.addClass("ui-button-text-only")}}});a.widget("ui.buttonset",{_create:function(){this.element.addClass("ui-buttonset");this._init()},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c);a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){this.buttons=this.element.find(":button, :submit, :reset, :checkbox, :radio, a, :data(button)").filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":visible").filter(":first").addClass("ui-corner-left").end().filter(":last").addClass("ui-corner-right").end().end().end()},
+destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy");a.Widget.prototype.destroy.call(this)}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.core.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.core.min.js
new file mode 100644
index 0000000..0f75491
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.core.min.js
@@ -0,0 +1,17 @@
+/*!
+ * jQuery UI 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI
+ */
+(function(c,j){function k(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.5",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,
+NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,
+"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");
+if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"));if(!isNaN(b)&&b!=0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,l,m){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(l)g-=parseFloat(c.curCSS(f,
+"border"+this+"Width",true))||0;if(m)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c.style(this,h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c.style(this,
+h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){var b=a.nodeName.toLowerCase(),d=c.attr(a,"tabindex");if("area"===b){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&k(a)}return(/input|select|textarea|button|object/.test(b)?!a.disabled:"a"==b?a.href||!isNaN(d):!isNaN(d))&&k(a)},tabbable:function(a){var b=c.attr(a,"tabindex");return(isNaN(b)||b>=0)&&c(a).is(":focusable")}});
+c(function(){var a=document.createElement("div"),b=document.body;c.extend(a.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.appendChild(a).offsetHeight===100;b.removeChild(a).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element,
+d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&&c.ui.isOverAxis(b,e,i)}})}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.dialog.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.dialog.min.js
new file mode 100644
index 0000000..0a8d035
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.dialog.min.js
@@ -0,0 +1,39 @@
+/*
+ * jQuery UI Dialog 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ * jquery.ui.button.js
+ * jquery.ui.draggable.js
+ * jquery.ui.mouse.js
+ * jquery.ui.position.js
+ * jquery.ui.resizable.js
+ */
+(function(c,j){c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:{my:"center",at:"center",of:window,collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");
+if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",f=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("<div></div>")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog",
+"aria-labelledby":f}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var e=(a.uiDialogTitlebar=c("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),h=c('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);
+return false}).appendTo(e);(a.uiDialogTitlebarCloseText=c("<span></span>")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("<span></span>").addClass("ui-dialog-title").attr("id",f).html(d).prependTo(e);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;e.find("*").add(e).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&
+g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");
+b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==b.uiDialog[0])d=Math.max(d,c(this).css("z-index"))});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,f=d.options;if(f.modal&&!a||!f.stack&&!f.modal)return d._trigger("focus",b);if(f.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=
+f.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.attr("scrollTop"),scrollLeft:d.element.attr("scrollLeft")};c.ui.dialog.maxZ+=1;d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;d.next().length&&d.appendTo("body");a._size();a._position(b.position);d.show(b.show);
+a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(f){if(f.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),e=g.filter(":first");g=g.filter(":last");if(f.target===g[0]&&!f.shiftKey){e.focus(1);return false}else if(f.target===e[0]&&f.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,
+f=c("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("<div></div>").addClass("ui-dialog-buttonset").appendTo(f);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,function(){return!(d=true)});if(d){c.each(a,function(e,h){h=c.isFunction(h)?{click:h,text:e}:h;e=c("<button></button>",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position,
+offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);
+b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(),
+handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,
+a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a);
+f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b?
+d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&&
+d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight-
+b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),
+function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()<c.ui.dialog.overlay.maxZ)return false})},1);c(document).bind("keydown.dialog-overlay",function(d){if(a.options.closeOnEscape&&d.keyCode&&d.keyCode===c.ui.keyCode.ESCAPE){a.close(d);d.preventDefault()}});c(window).bind("resize.dialog-overlay",c.ui.dialog.overlay.resize)}var b=
+(this.oldInstances.pop()||c("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a,
+b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a<b?c(window).height()+"px":a+"px"}else return c(document).height()+"px"},width:function(){var a,b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth);b=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth);return a<
+b?c(window).width()+"px":a+"px"}else return c(document).width()+"px"},resize:function(){var a=c([]);c.each(c.ui.dialog.overlay.instances,function(){a=a.add(this)});a.css({width:0,height:0}).css({width:c.ui.dialog.overlay.width(),height:c.ui.dialog.overlay.height()})}});c.extend(c.ui.dialog.overlay.prototype,{destroy:function(){c.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.draggable.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.draggable.min.js
new file mode 100644
index 0000000..141c6e1
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.draggable.min.js
@@ -0,0 +1,49 @@
+/*
+ * jQuery UI Draggable 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Draggables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper==
+"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b=
+this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;return true},_mouseStart:function(a){var b=this.options;this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-
+this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();
+d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);return true},_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||
+this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b=false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if(!this.element[0]||!this.element[0].parentNode)return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,
+b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle||!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==
+a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone():this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&&a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||
+0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],
+this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-
+(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment==
+"parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&
+a.containment.constructor!=Array){var b=d(a.containment)[0];if(b){a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),
+10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],
+this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():
+f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,g=a.pageY;if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.left<this.containment[0])e=this.containment[0]+this.offset.click.left;if(a.pageY-this.offset.click.top<this.containment[1])g=this.containment[1]+
+this.offset.click.top;if(a.pageX-this.offset.click.left>this.containment[2])e=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:!(g-this.offset.click.top<this.containment[1])?g-b.grid[1]:g+b.grid[1]:g;e=this.originalPageX+
+Math.round((e-this.originalPageX)/b.grid[0])*b.grid[0];e=this.containment?!(e-this.offset.click.left<this.containment[0]||e-this.offset.click.left>this.containment[2])?e:!(e-this.offset.click.left<this.containment[0])?e-b.grid[0]:e+b.grid[0]:e}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop()),left:e-this.offset.click.left-
+this.offset.relative.left-this.offset.parent.left+(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging");this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove();this.helper=null;this.cancelHelperRemoval=false},_trigger:function(a,b,c){c=c||this._uiHash();d.ui.plugin.call(this,a,[b,c]);if(a=="drag")this.positionAbs=
+this._convertPositionTo("absolute");return d.Widget.prototype._trigger.call(this,a,b,c)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}});d.extend(d.ui.draggable,{version:"1.8.5"});d.ui.plugin.add("draggable","connectToSortable",{start:function(a,b){var c=d(this).data("draggable"),f=c.options,e=d.extend({},b,{item:c.element});c.sortables=[];d(f.connectToSortable).each(function(){var g=d.data(this,"sortable");
+if(g&&!g.options.disabled){c.sortables.push({instance:g,shouldRevert:g.options.revert});g._refreshItems();g._trigger("activate",a,e)}})},stop:function(a,b){var c=d(this).data("draggable"),f=d.extend({},b,{item:c.element});d.each(c.sortables,function(){if(this.instance.isOver){this.instance.isOver=0;c.cancelHelperRemoval=true;this.instance.cancelHelperRemoval=false;if(this.shouldRevert)this.instance.options.revert=true;this.instance._mouseStop(a);this.instance.options.helper=this.instance.options._helper;
+c.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})}else{this.instance.cancelHelperRemoval=false;this.instance._trigger("deactivate",a,f)}})},drag:function(a,b){var c=d(this).data("draggable"),f=this;d.each(c.sortables,function(){this.instance.positionAbs=c.positionAbs;this.instance.helperProportions=c.helperProportions;this.instance.offset.click=c.offset.click;if(this.instance._intersectsWith(this.instance.containerCache)){if(!this.instance.isOver){this.instance.isOver=
+1;this.instance.currentItem=d(f).clone().appendTo(this.instance.element).data("sortable-item",true);this.instance.options._helper=this.instance.options.helper;this.instance.options.helper=function(){return b.helper[0]};a.target=this.instance.currentItem[0];this.instance._mouseCapture(a,true);this.instance._mouseStart(a,true,true);this.instance.offset.click.top=c.offset.click.top;this.instance.offset.click.left=c.offset.click.left;this.instance.offset.parent.left-=c.offset.parent.left-this.instance.offset.parent.left;
+this.instance.offset.parent.top-=c.offset.parent.top-this.instance.offset.parent.top;c._trigger("toSortable",a);c.dropped=this.instance.element;c.currentItem=c.element;this.instance.fromOutside=c}this.instance.currentItem&&this.instance._mouseDrag(a)}else if(this.instance.isOver){this.instance.isOver=0;this.instance.cancelHelperRemoval=true;this.instance.options.revert=false;this.instance._trigger("out",a,this.instance._uiHash(this.instance));this.instance._mouseStop(a,true);this.instance.options.helper=
+this.instance.options._helper;this.instance.currentItem.remove();this.instance.placeholder&&this.instance.placeholder.remove();c._trigger("fromSortable",a);c.dropped=false}})}});d.ui.plugin.add("draggable","cursor",{start:function(){var a=d("body"),b=d(this).data("draggable").options;if(a.css("cursor"))b._cursor=a.css("cursor");a.css("cursor",b.cursor)},stop:function(){var a=d(this).data("draggable").options;a._cursor&&d("body").css("cursor",a._cursor)}});d.ui.plugin.add("draggable","iframeFix",{start:function(){var a=
+d(this).data("draggable").options;d(a.iframeFix===true?"iframe":a.iframeFix).each(function(){d('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")})},stop:function(){d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;
+if(a.css("opacity"))b._opacity=a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!=document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!=
+"HTML"){if(!c.axis||c.axis!="x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY<c.scrollSensitivity)b.scrollParent[0].scrollTop=f=b.scrollParent[0].scrollTop+c.scrollSpeed;else if(a.pageY-b.overflowOffset.top<c.scrollSensitivity)b.scrollParent[0].scrollTop=f=b.scrollParent[0].scrollTop-c.scrollSpeed;if(!c.axis||c.axis!="y")if(b.overflowOffset.left+b.scrollParent[0].offsetWidth-a.pageX<c.scrollSensitivity)b.scrollParent[0].scrollLeft=f=b.scrollParent[0].scrollLeft+c.scrollSpeed;else if(a.pageX-
+b.overflowOffset.left<c.scrollSensitivity)b.scrollParent[0].scrollLeft=f=b.scrollParent[0].scrollLeft-c.scrollSpeed}else{if(!c.axis||c.axis!="x")if(a.pageY-d(document).scrollTop()<c.scrollSensitivity)f=d(document).scrollTop(d(document).scrollTop()-c.scrollSpeed);else if(d(window).height()-(a.pageY-d(document).scrollTop())<c.scrollSensitivity)f=d(document).scrollTop(d(document).scrollTop()+c.scrollSpeed);if(!c.axis||c.axis!="y")if(a.pageX-d(document).scrollLeft()<c.scrollSensitivity)f=d(document).scrollLeft(d(document).scrollLeft()-
+c.scrollSpeed);else if(d(window).width()-(a.pageX-d(document).scrollLeft())<c.scrollSensitivity)f=d(document).scrollLeft(d(document).scrollLeft()+c.scrollSpeed)}f!==false&&d.ui.ddmanager&&!c.dropBehaviour&&d.ui.ddmanager.prepareOffsets(b,a)}});d.ui.plugin.add("draggable","snap",{start:function(){var a=d(this).data("draggable"),b=a.options;a.snapElements=[];d(b.snap.constructor!=String?b.snap.items||":data(draggable)":b.snap).each(function(){var c=d(this),f=c.offset();this!=a.element[0]&&a.snapElements.push({item:this,
+width:c.outerWidth(),height:c.outerHeight(),top:f.top,left:f.left})})},drag:function(a,b){for(var c=d(this).data("draggable"),f=c.options,e=f.snapTolerance,g=b.offset.left,n=g+c.helperProportions.width,m=b.offset.top,o=m+c.helperProportions.height,h=c.snapElements.length-1;h>=0;h--){var i=c.snapElements[h].left,k=i+c.snapElements[h].width,j=c.snapElements[h].top,l=j+c.snapElements[h].height;if(i-e<g&&g<k+e&&j-e<m&&m<l+e||i-e<g&&g<k+e&&j-e<o&&o<l+e||i-e<n&&n<k+e&&j-e<m&&m<l+e||i-e<n&&n<k+e&&j-e<o&&
+o<l+e){if(f.snapMode!="inner"){var p=Math.abs(j-o)<=e,q=Math.abs(l-m)<=e,r=Math.abs(i-n)<=e,s=Math.abs(k-g)<=e;if(p)b.position.top=c._convertPositionTo("relative",{top:j-c.helperProportions.height,left:0}).top-c.margins.top;if(q)b.position.top=c._convertPositionTo("relative",{top:l,left:0}).top-c.margins.top;if(r)b.position.left=c._convertPositionTo("relative",{top:0,left:i-c.helperProportions.width}).left-c.margins.left;if(s)b.position.left=c._convertPositionTo("relative",{top:0,left:k}).left-c.margins.left}var t=
+p||q||r||s;if(f.snapMode!="outer"){p=Math.abs(j-m)<=e;q=Math.abs(l-o)<=e;r=Math.abs(i-g)<=e;s=Math.abs(k-n)<=e;if(p)b.position.top=c._convertPositionTo("relative",{top:j,left:0}).top-c.margins.top;if(q)b.position.top=c._convertPositionTo("relative",{top:l-c.helperProportions.height,left:0}).top-c.margins.top;if(r)b.position.left=c._convertPositionTo("relative",{top:0,left:i}).left-c.margins.left;if(s)b.position.left=c._convertPositionTo("relative",{top:0,left:k-c.helperProportions.width}).left-c.margins.left}if(!c.snapElements[h].snapping&&
+(p||q||r||s||t))c.options.snap.snap&&c.options.snap.snap.call(c.element,a,d.extend(c._uiHash(),{snapItem:c.snapElements[h].item}));c.snapElements[h].snapping=p||q||r||s||t}else{c.snapElements[h].snapping&&c.options.snap.release&&c.options.snap.release.call(c.element,a,d.extend(c._uiHash(),{snapItem:c.snapElements[h].item}));c.snapElements[h].snapping=false}}}});d.ui.plugin.add("draggable","stack",{start:function(){var a=d(this).data("draggable").options;a=d.makeArray(d(a.stack)).sort(function(c,f){return(parseInt(d(c).css("zIndex"),
+10)||0)-(parseInt(d(f).css("zIndex"),10)||0)});if(a.length){var b=parseInt(a[0].style.zIndex)||0;d(a).each(function(c){this.style.zIndex=b+c});this[0].style.zIndex=b+a.length}}});d.ui.plugin.add("draggable","zIndex",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;if(a.css("zIndex"))b._zIndex=a.css("zIndex");a.css("zIndex",b.zIndex)},stop:function(a,b){a=d(this).data("draggable").options;a._zIndex&&d(b.helper).css("zIndex",a._zIndex)}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.droppable.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.droppable.min.js
new file mode 100644
index 0000000..05b5afe
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.droppable.min.js
@@ -0,0 +1,26 @@
+/*
+ * jQuery UI Droppable 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Droppables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ * jquery.ui.mouse.js
+ * jquery.ui.draggable.js
+ */
+(function(d){d.widget("ui.droppable",{widgetEventPrefix:"drop",options:{accept:"*",activeClass:false,addClasses:true,greedy:false,hoverClass:false,scope:"default",tolerance:"intersect"},_create:function(){var a=this.options,b=a.accept;this.isover=0;this.isout=1;this.accept=d.isFunction(b)?b:function(c){return c.is(b)};this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight};d.ui.ddmanager.droppables[a.scope]=d.ui.ddmanager.droppables[a.scope]||[];d.ui.ddmanager.droppables[a.scope].push(this);
+a.addClasses&&this.element.addClass("ui-droppable")},destroy:function(){for(var a=d.ui.ddmanager.droppables[this.options.scope],b=0;b<a.length;b++)a[b]==this&&a.splice(b,1);this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable");return this},_setOption:function(a,b){if(a=="accept")this.accept=d.isFunction(b)?b:function(c){return c.is(b)};d.Widget.prototype._setOption.apply(this,arguments)},_activate:function(a){var b=d.ui.ddmanager.current;this.options.activeClass&&
+this.element.addClass(this.options.activeClass);b&&this._trigger("activate",a,this.ui(b))},_deactivate:function(a){var b=d.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass);b&&this._trigger("deactivate",a,this.ui(b))},_over:function(a){var b=d.ui.ddmanager.current;if(!(!b||(b.currentItem||b.element)[0]==this.element[0]))if(this.accept.call(this.element[0],b.currentItem||b.element)){this.options.hoverClass&&this.element.addClass(this.options.hoverClass);
+this._trigger("over",a,this.ui(b))}},_out:function(a){var b=d.ui.ddmanager.current;if(!(!b||(b.currentItem||b.element)[0]==this.element[0]))if(this.accept.call(this.element[0],b.currentItem||b.element)){this.options.hoverClass&&this.element.removeClass(this.options.hoverClass);this._trigger("out",a,this.ui(b))}},_drop:function(a,b){var c=b||d.ui.ddmanager.current;if(!c||(c.currentItem||c.element)[0]==this.element[0])return false;var e=false;this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var g=
+d.data(this,"droppable");if(g.options.greedy&&!g.options.disabled&&g.options.scope==c.options.scope&&g.accept.call(g.element[0],c.currentItem||c.element)&&d.ui.intersect(c,d.extend(g,{offset:g.element.offset()}),g.options.tolerance)){e=true;return false}});if(e)return false;if(this.accept.call(this.element[0],c.currentItem||c.element)){this.options.activeClass&&this.element.removeClass(this.options.activeClass);this.options.hoverClass&&this.element.removeClass(this.options.hoverClass);this._trigger("drop",
+a,this.ui(c));return this.element}return false},ui:function(a){return{draggable:a.currentItem||a.element,helper:a.helper,position:a.position,offset:a.positionAbs}}});d.extend(d.ui.droppable,{version:"1.8.5"});d.ui.intersect=function(a,b,c){if(!b.offset)return false;var e=(a.positionAbs||a.position.absolute).left,g=e+a.helperProportions.width,f=(a.positionAbs||a.position.absolute).top,h=f+a.helperProportions.height,i=b.offset.left,k=i+b.proportions.width,j=b.offset.top,l=j+b.proportions.height;
+switch(c){case "fit":return i<=e&&g<=k&&j<=f&&h<=l;case "intersect":return i<e+a.helperProportions.width/2&&g-a.helperProportions.width/2<k&&j<f+a.helperProportions.height/2&&h-a.helperProportions.height/2<l;case "pointer":return d.ui.isOver((a.positionAbs||a.position.absolute).top+(a.clickOffset||a.offset.click).top,(a.positionAbs||a.position.absolute).left+(a.clickOffset||a.offset.click).left,j,i,b.proportions.height,b.proportions.width);case "touch":return(f>=j&&f<=l||h>=j&&h<=l||f<j&&h>l)&&(e>=
+i&&e<=k||g>=i&&g<=k||e<i&&g>k);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f<c.length;f++)if(!(c[f].options.disabled||a&&!c[f].accept.call(c[f].element[0],a.currentItem||a.element))){for(var h=0;h<g.length;h++)if(g[h]==c[f].element[0]){c[f].proportions.height=0;continue a}c[f].visible=c[f].element.css("display")!=
+"none";if(c[f].visible){c[f].offset=c[f].element.offset();c[f].proportions={width:c[f].element[0].offsetWidth,height:c[f].element[0].offsetHeight};e=="mousedown"&&c[f]._activate.call(c[f],b)}}},drop:function(a,b){var c=false;d.each(d.ui.ddmanager.droppables[a.options.scope]||[],function(){if(this.options){if(!this.options.disabled&&this.visible&&d.ui.intersect(a,this,this.options.tolerance))c=c||this._drop.call(this,b);if(!this.options.disabled&&this.visible&&this.accept.call(this.element[0],a.currentItem||
+a.element)){this.isout=1;this.isover=0;this._deactivate.call(this,b)}}});return c},drag:function(a,b){a.options.refreshPositions&&d.ui.ddmanager.prepareOffsets(a,b);d.each(d.ui.ddmanager.droppables[a.options.scope]||[],function(){if(!(this.options.disabled||this.greedyChild||!this.visible)){var c=d.ui.intersect(a,this,this.options.tolerance);if(c=!c&&this.isover==1?"isout":c&&this.isover==0?"isover":null){var e;if(this.options.greedy){var g=this.element.parents(":data(droppable):eq(0)");if(g.length){e=
+d.data(g[0],"droppable");e.greedyChild=c=="isover"?1:0}}if(e&&c=="isover"){e.isover=0;e.isout=1;e._out.call(e,b)}this[c]=1;this[c=="isout"?"isover":"isout"]=0;this[c=="isover"?"_over":"_out"].call(this,b);if(e&&c=="isout"){e.isout=0;e.isover=1;e._over.call(e,b)}}}})}}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.mouse.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.mouse.min.js
new file mode 100644
index 0000000..d213979
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.mouse.min.js
@@ -0,0 +1,17 @@
+/*!
+ * jQuery UI Mouse 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Mouse
+ *
+ * Depends:
+ * jquery.ui.widget.js
+ */
+(function(c){c.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(b){return a._mouseDown(b)}).bind("click."+this.widgetName,function(b){if(a._preventClickEvent){a._preventClickEvent=false;b.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName)},_mouseDown:function(a){a.originalEvent=a.originalEvent||{};if(!a.originalEvent.mouseHandled){this._mouseStarted&&
+this._mouseUp(a);this._mouseDownEvent=a;var b=this,e=a.which==1,f=typeof this.options.cancel=="string"?c(a.target).parents().add(a.target).filter(this.options.cancel).length:false;if(!e||f||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){b.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==false;if(!this._mouseStarted){a.preventDefault();
+return true}}this._mouseMoveDelegate=function(d){return b._mouseMove(d)};this._mouseUpDelegate=function(d){return b._mouseUp(d)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);c.browser.safari||a.preventDefault();return a.originalEvent.mouseHandled=true}},_mouseMove:function(a){if(c.browser.msie&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&
+this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=a.target==this._mouseDownEvent.target;this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-
+a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.position.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.position.min.js
new file mode 100644
index 0000000..a273286
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.position.min.js
@@ -0,0 +1,16 @@
+/*
+ * jQuery UI Position 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Position
+ */
+(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.scrollTo&&d.document){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j=
+{top:b.of.pageY,left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/
+2;if(b.at[1]==="bottom")j.top+=k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+parseInt(c.curCSS(this,"marginRight",true))||0,w=m+q+parseInt(c.curCSS(this,"marginBottom",true))||0,i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]===
+"center")i.top-=m/2;i.left=parseInt(i.left);i.top=parseInt(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();
+b.left=d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];
+b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=
+c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.progressbar.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.progressbar.min.js
new file mode 100644
index 0000000..de2e8cb
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.progressbar.min.js
@@ -0,0 +1,16 @@
+/*
+ * jQuery UI Progressbar 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function(b,c){b.widget("ui.progressbar",{options:{value:0},min:0,max:100,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.max,"aria-valuenow":this._value()});this.valueDiv=b("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element);this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow");
+this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===c)return this._value();this._setOption("value",a);return this},_setOption:function(a,d){if(a==="value"){this.options.value=d;this._refreshValue();this._trigger("change")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.max,Math.max(this.min,a))},_refreshValue:function(){var a=this.value();this.valueDiv.toggleClass("ui-corner-right",
+a===this.max).width(a+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.5"})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.resizable.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.resizable.min.js
new file mode 100644
index 0000000..ab331d2
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.resizable.min.js
@@ -0,0 +1,47 @@
+/*
+ * jQuery UI Resizable 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function(e){e.widget("ui.resizable",e.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:false,animate:false,animateDuration:"slow",animateEasing:"swing",aspectRatio:false,autoHide:false,containment:false,ghost:false,grid:false,handles:"e,s,se",helper:false,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1E3},_create:function(){var b=this,a=this.options;this.element.addClass("ui-resizable");e.extend(this,{_aspectRatio:!!a.aspectRatio,aspectRatio:a.aspectRatio,originalElement:this.element,
+_proportionallyResizeElements:[],_helper:a.helper||a.ghost||a.animate?a.helper||"ui-resizable-helper":null});if(this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)){/relative/.test(this.element.css("position"))&&e.browser.opera&&this.element.css({position:"relative",top:"auto",left:"auto"});this.element.wrap(e('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),
+top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle=
+this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",
+nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d<c.length;d++){var f=e.trim(c[d]),g=e('<div class="ui-resizable-handle '+("ui-resizable-"+f)+'"></div>');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor==
+String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),k=0;k=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,k);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection();
+this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){e(this).removeClass("ui-resizable-autohide");b._handles.show()},function(){if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};
+if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a=false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),
+d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"});this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=
+this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:
+this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis];if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",
+b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false},_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;
+f={width:c.size.width-(f?0:c.sizeDiff.width),height:c.size.height-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f,{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",
+b);this._helper&&this.helper.remove();return false},_updateCache:function(b){this.offset=this.helper.offset();if(l(b.left))this.position.left=b.left;if(l(b.top))this.position.top=b.top;if(l(b.height))this.size.height=b.height;if(l(b.width))this.size.width=b.width},_updateRatio:function(b){var a=this.position,c=this.size,d=this.axis;if(b.height)b.width=c.height*this.aspectRatio;else if(b.width)b.height=c.width/this.aspectRatio;if(d=="sw"){b.left=a.left+(c.width-b.width);b.top=null}if(d=="nw"){b.top=
+a.top+(c.height-b.height);b.left=a.left+(c.width-b.width)}return b},_respectSize:function(b){var a=this.options,c=this.axis,d=l(b.width)&&a.maxWidth&&a.maxWidth<b.width,f=l(b.height)&&a.maxHeight&&a.maxHeight<b.height,g=l(b.width)&&a.minWidth&&a.minWidth>b.width,h=l(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,
+k=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&k)b.left=i-a.minWidth;if(d&&k)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left=null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a<this._proportionallyResizeElements.length;a++){var c=this._proportionallyResizeElements[a];if(!this.borderDif){var d=[c.css("borderTopWidth"),
+c.css("borderRightWidth"),c.css("borderBottomWidth"),c.css("borderLeftWidth")],f=[c.css("paddingTop"),c.css("paddingRight"),c.css("paddingBottom"),c.css("paddingLeft")];this.borderDif=e.map(d,function(g,h){g=parseInt(g,10)||0;h=parseInt(f[h],10)||0;return g+h})}e.browser.msie&&(e(b).is(":hidden")||e(b).parents(":hidden").length)||c.css({height:b.height()-this.borderDif[0]-this.borderDif[2]||0,width:b.width()-this.borderDif[1]-this.borderDif[3]||0})}},_renderProxy:function(){var b=this.options;this.elementOffset=
+this.element.offset();if(this._helper){this.helper=this.helper||e('<div style="overflow:hidden;"></div>');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+
+a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,
+arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]);b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,
+{version:"1.8.5"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(),10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,
+function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top-f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var k=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:k.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=
+(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(k.css("position"))){c._revertToRelativePosition=true;k.css({position:"absolute",top:"auto",left:"auto"})}k.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType?e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=
+false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a=e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-
+a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing,step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",
+b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement=e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top",
+"Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset;var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,
+f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left:a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=
+a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top-d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+
+a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition,f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&
+e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25,display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",
+height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b=e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=
+d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},l=function(b){return!isNaN(parseInt(b,10))}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.selectable.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.selectable.min.js
new file mode 100644
index 0000000..f632b3d
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.selectable.min.js
@@ -0,0 +1,22 @@
+/*
+ * jQuery UI Selectable 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"),
+selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("<div class='ui-selectable-helper'></div>")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX,
+c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting",
+c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d=
+this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.right<b||a.top>i||a.bottom<g);else if(d.tolerance=="fit")k=a.left>b&&a.right<h&&a.top>g&&a.bottom<i;if(k){if(a.selected){a.$element.removeClass("ui-selected");a.selected=false}if(a.unselecting){a.$element.removeClass("ui-unselecting");
+a.unselecting=false}if(!a.selecting){a.$element.addClass("ui-selecting");a.selecting=true;f._trigger("selecting",c,{selecting:a.element})}}else{if(a.selecting)if(c.metaKey&&a.startselected){a.$element.removeClass("ui-selecting");a.selecting=false;a.$element.addClass("ui-selected");a.selected=true}else{a.$element.removeClass("ui-selecting");a.selecting=false;if(a.startselected){a.$element.addClass("ui-unselecting");a.unselecting=true}f._trigger("unselecting",c,{unselecting:a.element})}if(a.selected)if(!c.metaKey&&
+!a.startselected){a.$element.removeClass("ui-selected");a.selected=false;a.$element.addClass("ui-unselecting");a.unselecting=true;f._trigger("unselecting",c,{unselecting:a.element})}}}});return false}},_mouseStop:function(c){var f=this;this.dragged=false;e(".ui-unselecting",this.element[0]).each(function(){var d=e.data(this,"selectable-item");d.$element.removeClass("ui-unselecting");d.unselecting=false;d.startselected=false;f._trigger("unselected",c,{unselected:d.element})});e(".ui-selecting",this.element[0]).each(function(){var d=
+e.data(this,"selectable-item");d.$element.removeClass("ui-selecting").addClass("ui-selected");d.selecting=false;d.selected=true;d.startselected=true;f._trigger("selected",c,{selected:d.element})});this._trigger("stop",c);this.helper.remove();return false}});e.extend(e.ui.selectable,{version:"1.8.5"})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.slider.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.slider.min.js
new file mode 100644
index 0000000..85ed454
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.slider.min.js
@@ -0,0 +1,33 @@
+/*
+ * jQuery UI Slider 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function(d){d.widget("ui.slider",d.ui.mouse,{widgetEventPrefix:"slide",options:{animate:false,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null},_create:function(){var a=this,b=this.options;this._mouseSliding=this._keySliding=false;this._animateOff=true;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");b.disabled&&this.element.addClass("ui-slider-disabled ui-disabled");
+this.range=d([]);if(b.range){if(b.range===true){this.range=d("<div></div>");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("<div></div>");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("<a href='#'></a>").appendTo(this.element).addClass("ui-slider-handle");
+if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length<b.values.length;)d("<a href='#'></a>").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur();
+else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e=
+false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h===
+a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");
+this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a,
+g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b=
+this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b=
+this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);
+c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c<e))c=e;if(c!==this.values(b)){e=this.values();e[b]=c;a=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e});this.values(b?0:1);a!==false&&this.values(b,c,true)}}else if(c!==this.value()){a=this._trigger("slide",a,{handle:this.handles[b],value:c});
+a!==false&&this.value(c)}},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);c.values=this.values()}this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b);c.values=this.values()}this._trigger("change",a,c)}},value:function(a){if(arguments.length){this.options.value=
+this._trimAlignValue(a);this._refreshValue();this._change(null,0)}return this._value()},values:function(a,b){var c,e,f;if(arguments.length>1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;f<c.length;f+=1){c[f]=this._trimAlignValue(e[f]);this._change(null,f)}this._refreshValue()}else return this.options.values&&this.options.values.length?this._values(a):this.value();
+else return this._values()},_setOption:function(a,b){var c,e=0;if(d.isArray(this.options.values))e=this.options.values.length;d.Widget.prototype._setOption.apply(this,arguments);switch(a){case "disabled":if(b){this.handles.filter(".ui-state-focus").blur();this.handles.removeClass("ui-state-hover");this.handles.attr("disabled","disabled");this.element.addClass("ui-disabled")}else{this.handles.removeAttr("disabled");this.element.removeClass("ui-disabled")}break;case "orientation":this._detectOrientation();
+this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation);this._refreshValue();break;case "value":this._animateOff=true;this._refreshValue();this._change(null,0);this._animateOff=false;break;case "values":this._animateOff=true;this._refreshValue();for(c=0;c<e;c+=1)this._change(null,c);this._animateOff=false;break}},_value:function(){var a=this.options.value;return a=this._trimAlignValue(a)},_values:function(a){var b,c;if(arguments.length){b=this.options.values[a];
+return b=this._trimAlignValue(b)}else{b=this.options.values.slice();for(c=0;c<b.length;c+=1)b[c]=this._trimAlignValue(b[c]);return b}},_trimAlignValue:function(a){if(a<this._valueMin())return this._valueMin();if(a>this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a=
+this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f-
+g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},
+b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.5"})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.sortable.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.sortable.min.js
new file mode 100644
index 0000000..ca8ab74
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.sortable.min.js
@@ -0,0 +1,60 @@
+/*
+ * jQuery UI Sortable 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Sortables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */
+(function(d){d.widget("ui.sortable",d.ui.mouse,{widgetEventPrefix:"sort",options:{appendTo:"parent",axis:false,connectWith:false,containment:false,cursor:"auto",cursorAt:false,dropOnEmpty:true,forcePlaceholderSize:false,forceHelperSize:false,grid:false,handle:false,helper:"original",items:"> *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){this.containerCache={};this.element.addClass("ui-sortable");
+this.refresh();this.floating=this.items.length?/left|right/.test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a==="disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this,
+arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&&!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem=
+c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset,
+{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment();
+if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start",
+a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a);return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");
+if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY<b.scrollSensitivity)this.scrollParent[0].scrollTop=c=this.scrollParent[0].scrollTop+b.scrollSpeed;else if(a.pageY-this.overflowOffset.top<b.scrollSensitivity)this.scrollParent[0].scrollTop=c=this.scrollParent[0].scrollTop-b.scrollSpeed;if(this.overflowOffset.left+
+this.scrollParent[0].offsetWidth-a.pageX<b.scrollSensitivity)this.scrollParent[0].scrollLeft=c=this.scrollParent[0].scrollLeft+b.scrollSpeed;else if(a.pageX-this.overflowOffset.left<b.scrollSensitivity)this.scrollParent[0].scrollLeft=c=this.scrollParent[0].scrollLeft-b.scrollSpeed}else{if(a.pageY-d(document).scrollTop()<b.scrollSensitivity)c=d(document).scrollTop(d(document).scrollTop()-b.scrollSpeed);else if(d(window).height()-(a.pageY-d(document).scrollTop())<b.scrollSensitivity)c=d(document).scrollTop(d(document).scrollTop()+
+b.scrollSpeed);if(a.pageX-d(document).scrollLeft()<b.scrollSensitivity)c=d(document).scrollLeft(d(document).scrollLeft()-b.scrollSpeed);else if(d(window).width()-(a.pageX-d(document).scrollLeft())<b.scrollSensitivity)c=d(document).scrollLeft(d(document).scrollLeft()+b.scrollSpeed)}c!==false&&d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a)}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+
+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";for(b=this.items.length-1;b>=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0],e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a,
+c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset();c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]==
+document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp();this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate",
+null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem):
+d(this.domPosition.parent).prepend(this.currentItem);return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")},toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute||
+"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+j<k&&b+l>g&&b+l<h;return this.options.tolerance=="pointer"||this.options.forcePointerForContainers||this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>a[this.floating?"width":"height"]?j:g<b+
+this.helperProportions.width/2&&c-this.helperProportions.width/2<h&&i<e+this.helperProportions.height/2&&f-this.helperProportions.height/2<k},_intersectsWithPointer:function(a){var b=d.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,a.top,a.height);a=d.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,a.left,a.width);b=b&&a;a=this._getDragVerticalDirection();var c=this._getDragHorizontalDirection();if(!b)return false;return this.floating?c&&c=="right"||a=="down"?2:1:a&&(a=="down"?
+2:1)},_intersectsWithSides:function(a){var b=d.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,a.top+a.height/2,a.height);a=d.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,a.left+a.width/2,a.width);var c=this._getDragVerticalDirection(),e=this._getDragHorizontalDirection();return this.floating&&e?e=="right"&&a||e=="left"&&!a:c&&(c=="down"&&b||c=="up"&&!b)},_getDragVerticalDirection:function(){var a=this.positionAbs.top-this.lastPositionAbs.top;return a!=0&&(a>0?"down":"up")},
+_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith();if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!=
+this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a=
+this.currentItem.find(":data(sortable-item)"),b=0;b<this.items.length;b++)for(var c=0;c<a.length;c++)a[c]==this.items[b].item[0]&&this.items.splice(b,1)},_refreshItems:function(a){this.items=[];this.containers=[this];var b=this.items,c=[[d.isFunction(this.options.items)?this.options.items.call(this.element[0],a,{item:this.currentItem}):d(this.options.items,this.element),this]],e=this._connectWith();if(e)for(var f=e.length-1;f>=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable");
+if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h<g;h++){i=d(e[h]);i.data("sortable-item",a);b.push({item:i,instance:a,width:0,height:0,left:0,top:0})}}},refreshPositions:function(a){if(this.offsetParent&&this.helper)this.offset.parent=this._getParentOffset();for(var b=this.items.length-1;b>=
+0;b--){var c=this.items[b],e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b=this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width=
+this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f=d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f},
+update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b=
+null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out",a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this));
+this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h-f)<b){b=Math.abs(h-f);e=this.items[g]}}if(e||this.options.dropOnEmpty){this.currentContainer=this.containers[c];e?this._rearrange(a,e,null,true):this._rearrange(a,
+null,this.containers[c].element,true);this._trigger("change",a,this._uiHash());this.containers[c]._trigger("change",a,this._uiHash(this));this.options.placeholder.update(this.currentContainer,this.placeholder);this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}}},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a,this.currentItem])):b.helper=="clone"?this.currentItem.clone():this.currentItem;a.parents("body").length||
+d(b.appendTo!="parent"?b.appendTo:this.currentItem[0].parentNode)[0].appendChild(a[0]);if(a[0]==this.currentItem[0])this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")};if(a[0].style.width==""||b.forceHelperSize)a.width(this.currentItem.width());if(a[0].style.height==""||b.forceHelperSize)a.height(this.currentItem.height());return a},_adjustOffsetFromHelper:function(a){if(typeof a==
+"string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition==
+"absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition==
+"relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},
+_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-
+this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)){var b=d(a.containment)[0];a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),
+10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?
+this.offsetParent:this.scrollParent,e=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=
+this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(c[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0]))this.offset.relative=this._getRelativeOffset();var f=a.pageX,g=a.pageY;if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.left<this.containment[0])f=this.containment[0]+
+this.offset.click.left;if(a.pageY-this.offset.click.top<this.containment[1])g=this.containment[1]+this.offset.click.top;if(a.pageX-this.offset.click.left>this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?
+g:!(g-this.offset.click.top<this.containment[1])?g-b.grid[1]:g+b.grid[1]:g;f=this.originalPageX+Math.round((f-this.originalPageX)/b.grid[0])*b.grid[0];f=this.containment?!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:!(f-this.offset.click.left<this.containment[0])?f-b.grid[0]:f+b.grid[0]:f}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(d.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():
+e?0:c.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(d.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:c.scrollLeft())}},_rearrange:function(a,b,c,e){c?c[0].appendChild(this.placeholder[0]):b.item[0].parentNode.insertBefore(this.placeholder[0],this.direction=="down"?b.item[0]:b.item[0].nextSibling);this.counter=this.counter?++this.counter:1;var f=this,g=this.counter;window.setTimeout(function(){g==
+f.counter&&f.refreshPositions(!e)},0)},_clear:function(a,b){this.reverting=false;var c=[];!this._noFinalSort&&this.currentItem[0].parentNode&&this.placeholder.before(this.currentItem);this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var e in this._storedCSS)if(this._storedCSS[e]=="auto"||this._storedCSS[e]=="static")this._storedCSS[e]="";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!b&&c.push(function(f){this._trigger("receive",
+f,this._uiHash(this.fromOutside))});if((this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!b)c.push(function(f){this._trigger("update",f,this._uiHash())});if(!d.ui.contains(this.element[0],this.currentItem[0])){b||c.push(function(f){this._trigger("remove",f,this._uiHash())});for(e=this.containers.length-1;e>=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive",
+g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this,this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over=
+0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop",a,this._uiHash());for(e=0;e<c.length;e++)c[e].call(this,a);this._trigger("stop",a,this._uiHash())}return false}b||this._trigger("beforeStop",a,this._uiHash());this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+this.helper[0]!=this.currentItem[0]&&this.helper.remove();this.helper=null;if(!b){for(e=0;e<c.length;e++)c[e].call(this,a);this._trigger("stop",a,this._uiHash())}this.fromOutside=false;return true},_trigger:function(){d.Widget.prototype._trigger.apply(this,arguments)===false&&this.cancel()},_uiHash:function(a){var b=a||this;return{helper:b.helper,placeholder:b.placeholder||d([]),position:b.position,originalPosition:b.originalPosition,offset:b.positionAbs,item:b.currentItem,sender:a?a.element:null}}});
+d.extend(d.ui.sortable,{version:"1.8.5"})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.tabs.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.tabs.min.js
new file mode 100644
index 0000000..b441c0b
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.tabs.min.js
@@ -0,0 +1,35 @@
+/*
+ * jQuery UI Tabs 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"<div></div>",remove:null,select:null,show:null,spinner:"<em>Loading…</em>",tabTemplate:"<li><a href='#{href}'><span>#{label}</span></a></li>"},_create:function(){this._tabify(true)},_setOption:function(a,e){if(a=="selected")this.options.collapsible&&
+e==this.options.selected||this.select(e);else{this.options[a]=e;this._tabify()}},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var a=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[a].concat(d.makeArray(arguments)))},_ui:function(a,e){return{tab:a,panel:e,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var a=
+d(this);a.html(a.data("label.tabs")).removeData("label.tabs")})},_tabify:function(a){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var b=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]||
+(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))b.panels=b.panels.add(b._sanitizeSelector(i));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=b._tabId(f);f.href="#"+i;f=d("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(b.panels[g-1]||b.list);f.data("destroy.tabs",true)}b.panels=b.panels.add(f)}else c.disabled.push(g)});if(a){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all");
+this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(b._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected=
+this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return b.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active");
+if(c.selected>=0&&this.anchors.length){this.panels.eq(c.selected).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");b.element.queue("tabs",function(){b._trigger("show",null,b._ui(b.anchors[c.selected],b.panels[c.selected]))});this.load(c.selected)}d(window).bind("unload",function(){b.lis.add(b.anchors).unbind(".tabs");b.lis=b.anchors=b.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"));this.element[c.collapsible?"addClass":
+"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);a=0;for(var j;j=this.lis[a];a++)d(j)[d.inArray(a,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+g)};this.lis.bind("mouseover.tabs",
+function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal",function(){e(f,o);b._trigger("show",
+null,b._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");b._trigger("show",null,b._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);b.element.dequeue("tabs")})}:function(g,f){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");b.element.dequeue("tabs")};this.anchors.bind(c.event+".tabs",
+function(){var g=this,f=d(g).closest("li"),i=b.panels.filter(":not(.ui-tabs-hide)"),l=d(b._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||b.panels.filter(":animated").length||b._trigger("select",null,b._ui(this,l[0]))===false){this.blur();return false}c.selected=b.anchors.index(this);b.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected=-1;c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs",
+function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this));this.blur();return false}c.cookie&&b._cookie(c.selected,c.cookie);if(l.length){i.length&&b.element.queue("tabs",function(){s(g,i)});b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier.";d.browser.msie&&this.blur()});this.anchors.bind("click.tabs",
+function(){return false})},_getIndex:function(a){if(typeof a=="string")a=this.anchors.index(this.anchors.filter("[href$="+a+"]"));return a},destroy:function(){var a=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e=d.data(this,"href.tabs");if(e)this.href=
+e;var b=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){b.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});a.cookie&&this._cookie(null,a.cookie);return this},add:function(a,e,b){if(b===p)b=this.anchors.length;
+var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,a).replace(/#\{label\}/g,e));a=!a.indexOf("#")?a.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=d("#"+a);j.length||(j=d(h.panelTemplate).attr("id",a).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(b>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[b]);
+j.insertBefore(this.panels[b])}h.disabled=d.map(h.disabled,function(k){return k>=b?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[b],this.panels[b]));return this},remove:function(a){a=this._getIndex(a);var e=this.options,b=this.lis.eq(a).remove(),c=this.panels.eq(a).remove();
+if(b.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(a+(a+1<this.anchors.length?1:-1));e.disabled=d.map(d.grep(e.disabled,function(h){return h!=a}),function(h){return h>=a?--h:h});this._tabify();this._trigger("remove",null,this._ui(b.find("a")[0],c[0]));return this},enable:function(a){a=this._getIndex(a);var e=this.options;if(d.inArray(a,e.disabled)!=-1){this.lis.eq(a).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(b){return b!=a});this._trigger("enable",null,
+this._ui(this.anchors[a],this.panels[a]));return this}},disable:function(a){a=this._getIndex(a);var e=this.options;if(a!=e.selected){this.lis.eq(a).addClass("ui-state-disabled");e.disabled.push(a);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))}return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this},
+load:function(a){a=this._getIndex(a);var e=this,b=this.options,c=this.anchors.eq(a)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(a).addClass("ui-state-processing");if(b.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(b.spinner)}this.xhr=d.ajax(d.extend({},b.ajaxOptions,{url:h,success:function(k,n){d(e._sanitizeSelector(c.hash)).html(k);e._cleanup();b.cache&&d.data(c,"cache.tabs",
+true);e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.error(k,n,a,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this},url:function(a,
+e){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.5"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(a,e){var b=this,c=this.options,h=b._rotate||(b._rotate=function(j){clearTimeout(b.rotation);b.rotation=setTimeout(function(){var k=c.selected;b.select(++k<b.anchors.length?k:0)},a);j&&j.stopPropagation()});e=b._unrotate||(b._unrotate=!e?function(j){j.clientX&&b.rotate(null)}:
+function(){t=c.selected;h()});if(a){this.element.bind("tabsshow",h);this.anchors.bind(c.event+".tabs",e);h()}else{clearTimeout(b.rotation);this.element.unbind("tabsshow",h);this.anchors.unbind(c.event+".tabs",e);delete this._rotate;delete this._unrotate}return this}})})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery.ui.widget.min.js b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.widget.min.js
new file mode 100644
index 0000000..0cde64c
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery.ui.widget.min.js
@@ -0,0 +1,15 @@
+/*!
+ * jQuery UI Widget 1.8.5
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Widget
+ */
+(function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)b(d).triggerHandler("remove");k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){b(this).triggerHandler("remove")});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]=function(h){return!!b.data(h,
+a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)):d;if(e&&d.substring(0,1)===
+"_")return h;e?this.each(function(){var g=b.data(this,a);if(!g)throw"cannot call methods on "+a+" prior to initialization; attempted to call method '"+d+"'";if(!b.isFunction(g[d]))throw"no such method '"+d+"' for "+a+" widget instance";var i=g[d].apply(g,f);if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget",
+widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options=b.extend(true,{},this.options,b.metadata&&b.metadata.get(c)[this.widgetName],a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._init()},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+
+"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(a,c){var d=a,e=this;if(arguments.length===0)return b.extend({},e.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}b.each(d,function(f,h){e._setOption(f,h)});return e},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",c);return this},enable:function(){return this._setOption("disabled",
+false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery);
diff --git a/azkaban-webserver/src/web/js/jqueryui/jquery-ui-sliderAccess.js b/azkaban-webserver/src/web/js/jqueryui/jquery-ui-sliderAccess.js
new file mode 100644
index 0000000..a54cf4a
--- /dev/null
+++ b/azkaban-webserver/src/web/js/jqueryui/jquery-ui-sliderAccess.js
@@ -0,0 +1,89 @@
+/*
+ * jQuery UI Slider Access
+ * By: Trent Richardson [http://trentrichardson.com]
+ * Version 0.3
+ * Last Modified: 10/20/2012
+ *
+ * Copyright 2011 Trent Richardson
+ * Dual licensed under the MIT and GPL licenses.
+ * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt
+ * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt
+ *
+ */
+ (function($){
+
+ $.fn.extend({
+ sliderAccess: function(options){
+ options = options || {};
+ options.touchonly = options.touchonly !== undefined? options.touchonly : true; // by default only show it if touch device
+
+ if(options.touchonly === true && !("ontouchend" in document))
+ return $(this);
+
+ return $(this).each(function(i,obj){
+ var $t = $(this),
+ o = $.extend({},{
+ where: 'after',
+ step: $t.slider('option','step'),
+ upIcon: 'ui-icon-plus',
+ downIcon: 'ui-icon-minus',
+ text: false,
+ upText: '+',
+ downText: '-',
+ buttonset: true,
+ buttonsetTag: 'span',
+ isRTL: false
+ }, options),
+ $buttons = $('<'+ o.buttonsetTag +' class="ui-slider-access">'+
+ '<button data-icon="'+ o.downIcon +'" data-step="'+ (o.isRTL? o.step : o.step*-1) +'">'+ o.downText +'</button>'+
+ '<button data-icon="'+ o.upIcon +'" data-step="'+ (o.isRTL? o.step*-1 : o.step) +'">'+ o.upText +'</button>'+
+ '</'+ o.buttonsetTag +'>');
+
+ $buttons.children('button').each(function(j, jobj){
+ var $jt = $(this);
+ $jt.button({
+ text: o.text,
+ icons: { primary: $jt.data('icon') }
+ })
+ .click(function(e){
+ var step = $jt.data('step'),
+ curr = $t.slider('value'),
+ newval = curr += step*1,
+ minval = $t.slider('option','min'),
+ maxval = $t.slider('option','max'),
+ slidee = $t.slider("option", "slide") || function(){},
+ stope = $t.slider("option", "stop") || function(){};
+
+ e.preventDefault();
+
+ if(newval < minval || newval > maxval)
+ return;
+
+ $t.slider('value', newval);
+
+ slidee.call($t, null, { value: newval });
+ stope.call($t, null, { value: newval });
+ });
+ });
+
+ // before or after
+ $t[o.where]($buttons);
+
+ if(o.buttonset){
+ $buttons.removeClass('ui-corner-right').removeClass('ui-corner-left').buttonset();
+ $buttons.eq(0).addClass('ui-corner-left');
+ $buttons.eq(1).addClass('ui-corner-right');
+ }
+
+ // adjust the width so we don't break the original layout
+ var bOuterWidth = $buttons.css({
+ marginLeft: ((o.where == 'after' && !o.isRTL) || (o.where == 'before' && o.isRTL)? 10:0),
+ marginRight: ((o.where == 'before' && !o.isRTL) || (o.where == 'after' && o.isRTL)? 10:0)
+ }).outerWidth(true) + 5;
+ var tOuterWidth = $t.outerWidth(true);
+ $t.css('display','inline-block').width(tOuterWidth-bOuterWidth);
+ });
+ }
+ });
+
+})(jQuery);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/moment.min.js b/azkaban-webserver/src/web/js/moment.min.js
new file mode 100644
index 0000000..568ad05
--- /dev/null
+++ b/azkaban-webserver/src/web/js/moment.min.js
@@ -0,0 +1,6 @@
+//! moment.js
+//! version : 2.4.0
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+(function(a){function b(a,b){return function(c){return i(a.call(this,c),b)}}function c(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function d(){}function e(a){u(a),g(this,a)}function f(a){var b=o(a),c=b.year||0,d=b.month||0,e=b.week||0,f=b.day||0,g=b.hour||0,h=b.minute||0,i=b.second||0,j=b.millisecond||0;this._input=a,this._milliseconds=+j+1e3*i+6e4*h+36e5*g,this._days=+f+7*e,this._months=+d+12*c,this._data={},this._bubble()}function g(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function h(a){return 0>a?Math.ceil(a):Math.floor(a)}function i(a,b){for(var c=a+"";c.length<b;)c="0"+c;return c}function j(a,b,c,d){var e,f,g=b._milliseconds,h=b._days,i=b._months;g&&a._d.setTime(+a._d+g*c),(h||i)&&(e=a.minute(),f=a.hour()),h&&a.date(a.date()+h*c),i&&a.month(a.month()+i*c),g&&!d&&bb.updateOffset(a),(h||i)&&(a.minute(e),a.hour(f))}function k(a){return"[object Array]"===Object.prototype.toString.call(a)}function l(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function m(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++;return g+f}function n(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=Kb[a]||Lb[b]||b}return a}function o(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=n(c),b&&(d[b]=a[c]));return d}function p(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}bb[b]=function(e,f){var g,h,i=bb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=bb().utc().set(d,a);return i.call(bb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function q(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function r(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function s(a){return t(a)?366:365}function t(a){return 0===a%4&&0!==a%100||0===a%400}function u(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[gb]<0||a._a[gb]>11?gb:a._a[hb]<1||a._a[hb]>r(a._a[fb],a._a[gb])?hb:a._a[ib]<0||a._a[ib]>23?ib:a._a[jb]<0||a._a[jb]>59?jb:a._a[kb]<0||a._a[kb]>59?kb:a._a[lb]<0||a._a[lb]>999?lb:-1,a._pf._overflowDayOfYear&&(fb>b||b>hb)&&(b=hb),a._pf.overflow=b)}function v(a){a._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function w(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function x(a){return a?a.toLowerCase().replace("_","-"):a}function y(a,b){return b.abbr=a,mb[a]||(mb[a]=new d),mb[a].set(b),mb[a]}function z(a){delete mb[a]}function A(a){var b,c,d,e,f=0,g=function(a){if(!mb[a]&&nb)try{require("./lang/"+a)}catch(b){}return mb[a]};if(!a)return bb.fn._lang;if(!k(a)){if(c=g(a))return c;a=[a]}for(;f<a.length;){for(e=x(a[f]).split("-"),b=e.length,d=x(a[f+1]),d=d?d.split("-"):null;b>0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&m(e,d,!0)>=b-1)break;b--}f++}return bb.fn._lang}function B(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function C(a){var b,c,d=a.match(rb);for(b=0,c=d.length;c>b;b++)d[b]=Pb[d[b]]?Pb[d[b]]:B(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function D(a,b){return a.isValid()?(b=E(b,a.lang()),Mb[b]||(Mb[b]=C(b)),Mb[b](a)):a.lang().invalidDate()}function E(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(sb.lastIndex=0;d>=0&&sb.test(a);)a=a.replace(sb,c),sb.lastIndex=0,d-=1;return a}function F(a,b){var c;switch(a){case"DDDD":return vb;case"YYYY":case"GGGG":case"gggg":return wb;case"YYYYY":case"GGGGG":case"ggggg":return xb;case"S":case"SS":case"SSS":case"DDD":return ub;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return zb;case"a":case"A":return A(b._l)._meridiemParse;case"X":return Cb;case"Z":case"ZZ":return Ab;case"T":return Bb;case"SSSS":return yb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"ww":case"W":case"WW":case"e":case"E":return tb;default:return c=new RegExp(N(M(a.replace("\\","")),"i"))}}function G(a){var b=(Ab.exec(a)||[])[0],c=(b+"").match(Hb)||["-",0,0],d=+(60*c[1])+q(c[2]);return"+"===c[0]?-d:d}function H(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[gb]=q(b)-1);break;case"MMM":case"MMMM":d=A(c._l).monthsParse(b),null!=d?e[gb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[hb]=q(b));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=q(b));break;case"YY":e[fb]=q(b)+(q(b)>68?1900:2e3);break;case"YYYY":case"YYYYY":e[fb]=q(b);break;case"a":case"A":c._isPm=A(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[ib]=q(b);break;case"m":case"mm":e[jb]=q(b);break;case"s":case"ss":e[kb]=q(b);break;case"S":case"SS":case"SSS":case"SSSS":e[lb]=q(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=G(b);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":a=a.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=b)}}function I(a){var b,c,d,e,f,g,h,i,j,k,l=[];if(!a._d){for(d=K(a),a._w&&null==a._a[hb]&&null==a._a[gb]&&(f=function(b){return b?b.length<3?parseInt(b,10)>68?"19"+b:"20"+b:b:null==a._a[fb]?bb().weekYear():a._a[fb]},g=a._w,null!=g.GG||null!=g.W||null!=g.E?h=X(f(g.GG),g.W||1,g.E,4,1):(i=A(a._l),j=null!=g.d?T(g.d,i):null!=g.e?parseInt(g.e,10)+i._week.dow:0,k=parseInt(g.w,10)||1,null!=g.d&&j<i._week.dow&&k++,h=X(f(g.gg),k,j,i._week.doy,i._week.dow)),a._a[fb]=h.year,a._dayOfYear=h.dayOfYear),a._dayOfYear&&(e=null==a._a[fb]?d[fb]:a._a[fb],a._dayOfYear>s(e)&&(a._pf._overflowDayOfYear=!0),c=S(e,0,a._dayOfYear),a._a[gb]=c.getUTCMonth(),a._a[hb]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=l[b]=d[b];for(;7>b;b++)a._a[b]=l[b]=null==a._a[b]?2===b?1:0:a._a[b];l[ib]+=q((a._tzm||0)/60),l[jb]+=q((a._tzm||0)%60),a._d=(a._useUTC?S:R).apply(null,l)}}function J(a){var b;a._d||(b=o(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],I(a))}function K(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function L(a){a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=A(a._l),h=""+a._i,i=h.length,j=0;for(d=E(a._f,g).match(rb)||[],b=0;b<d.length;b++)e=d[b],c=(F(e,a).exec(h)||[])[0],c&&(f=h.substr(0,h.indexOf(c)),f.length>0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),Pb[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),H(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[ib]<12&&(a._a[ib]+=12),a._isPm===!1&&12===a._a[ib]&&(a._a[ib]=0),I(a),u(a)}function M(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function N(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function O(a){var b,c,d,e,f;if(0===a._f.length)return a._pf.invalidFormat=!0,a._d=new Date(0/0),void 0;for(e=0;e<a._f.length;e++)f=0,b=g({},a),v(b),b._f=a._f[e],L(b),w(b)&&(f+=b._pf.charsLeftOver,f+=10*b._pf.unusedTokens.length,b._pf.score=f,(null==d||d>f)&&(d=f,c=b));g(a,c||b)}function P(a){var b,c=a._i,d=Db.exec(c);if(d){for(a._pf.iso=!0,b=4;b>0;b--)if(d[b]){a._f=Fb[b-1]+(d[6]||" ");break}for(b=0;4>b;b++)if(Gb[b][1].exec(c)){a._f+=Gb[b][0];break}Ab.exec(c)&&(a._f+="Z"),L(a)}else a._d=new Date(c)}function Q(b){var c=b._i,d=ob.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?P(b):k(c)?(b._a=c.slice(0),I(b)):l(c)?b._d=new Date(+c):"object"==typeof c?J(b):b._d=new Date(c)}function R(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function S(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function T(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function U(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function V(a,b,c){var d=eb(Math.abs(a)/1e3),e=eb(d/60),f=eb(e/60),g=eb(f/24),h=eb(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",eb(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,U.apply({},i)}function W(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=bb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function X(a,b,c,d,e){var f,g,h=new Date(Date.UTC(a,0)).getUTCDay();return c=null!=c?c:e,f=e-h+(h>d?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:s(a-1)+g}}function Y(a){var b=a._i,c=a._f;return"undefined"==typeof a._pf&&v(a),null===b?bb.invalid({nullInput:!0}):("string"==typeof b&&(a._i=b=A().preparse(b)),bb.isMoment(b)?(a=g({},b),a._d=new Date(+b._d)):c?k(c)?O(a):L(a):Q(a),new e(a))}function Z(a,b){bb.fn[a]=bb.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),bb.updateOffset(this),this):this._d["get"+c+b]()}}function $(a){bb.duration.fn[a]=function(){return this._data[a]}}function _(a,b){bb.duration.fn["as"+a]=function(){return+this/b}}function ab(a){var b=!1,c=bb;"undefined"==typeof ender&&(this.moment=a?function(){return!b&&console&&console.warn&&(b=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),c.apply(null,arguments)}:bb)}for(var bb,cb,db="2.4.0",eb=Math.round,fb=0,gb=1,hb=2,ib=3,jb=4,kb=5,lb=6,mb={},nb="undefined"!=typeof module&&module.exports,ob=/^\/?Date\((\-?\d+)/i,pb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,qb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,rb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,sb=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,tb=/\d\d?/,ub=/\d{1,3}/,vb=/\d{3}/,wb=/\d{1,4}/,xb=/[+\-]?\d{1,6}/,yb=/\d+/,zb=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ab=/Z|[\+\-]\d\d:?\d\d/i,Bb=/T/i,Cb=/[\+\-]?\d+(\.\d{1,3})?/,Db=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/,Eb="YYYY-MM-DDTHH:mm:ssZ",Fb=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Gb=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Hb=/([\+\-]|\d\d)/gi,Ib="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Jb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Kb={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Lb={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Mb={},Nb="DDD w W M D d".split(" "),Ob="M D H h m s w W".split(" "),Pb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return i(this.year()%100,2)},YYYY:function(){return i(this.year(),4)},YYYYY:function(){return i(this.year(),5)},gg:function(){return i(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return i(this.weekYear(),5)},GG:function(){return i(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return i(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return q(this.milliseconds()/100)},SS:function(){return i(q(this.milliseconds()/10),2)},SSS:function(){return i(this.milliseconds(),3)},SSSS:function(){return i(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+":"+i(q(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(10*a/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}},Qb=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Nb.length;)cb=Nb.pop(),Pb[cb+"o"]=c(Pb[cb],cb);for(;Ob.length;)cb=Ob.pop(),Pb[cb+cb]=b(Pb[cb],2);for(Pb.DDDD=b(Pb.DDD,3),g(d.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=bb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=bb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return W(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),bb=function(b,c,d,e){return"boolean"==typeof d&&(e=d,d=a),Y({_i:b,_f:c,_l:d,_strict:e,_isUTC:!1})},bb.utc=function(b,c,d,e){var f;return"boolean"==typeof d&&(e=d,d=a),f=Y({_useUTC:!0,_isUTC:!0,_l:d,_i:b,_f:c,_strict:e}).utc()},bb.unix=function(a){return bb(1e3*a)},bb.duration=function(a,b){var c,d,e,g=bb.isDuration(a),h="number"==typeof a,i=g?a._input:h?{}:a,j=null;return h?b?i[b]=a:i.milliseconds=a:(j=pb.exec(a))?(c="-"===j[1]?-1:1,i={y:0,d:q(j[hb])*c,h:q(j[ib])*c,m:q(j[jb])*c,s:q(j[kb])*c,ms:q(j[lb])*c}):(j=qb.exec(a))&&(c="-"===j[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},i={y:e(j[2]),M:e(j[3]),d:e(j[4]),h:e(j[5]),m:e(j[6]),s:e(j[7]),w:e(j[8])}),d=new f(i),g&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},bb.version=db,bb.defaultFormat=Eb,bb.updateOffset=function(){},bb.lang=function(a,b){var c;return a?(b?y(x(a),b):null===b?(z(a),a="en"):mb[a]||A(a),c=bb.duration.fn._lang=bb.fn._lang=A(a),c._abbr):bb.fn._lang._abbr},bb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),A(a)},bb.isMoment=function(a){return a instanceof e},bb.isDuration=function(a){return a instanceof f},cb=Qb.length-1;cb>=0;--cb)p(Qb[cb]);for(bb.normalizeUnits=function(a){return n(a)},bb.invalid=function(a){var b=bb.utc(0/0);return null!=a?g(b._pf,a):b._pf.userInvalidated=!0,b},bb.parseZone=function(a){return bb(a).parseZone()},g(bb.fn=e.prototype,{clone:function(){return bb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return D(bb(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return w(this)},isDSTShifted:function(){return this._a?this.isValid()&&m(this._a,(this._isUTC?bb.utc(this._a):bb(this._a)).toArray())>0:!1},parsingFlags:function(){return g({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=D(this,a||bb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?bb.duration(+b,a):bb.duration(a,b),j(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?bb.duration(+b,a):bb.duration(a,b),j(this,c,-1),this},diff:function(a,b,c){var d,e,f=this._isUTC?bb(a).zone(this._offset||0):bb(a).local(),g=6e4*(this.zone()-f.zone());return b=n(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-bb(this).startOf("month")-(f-bb(f).startOf("month")))/d,e-=6e4*(this.zone()-bb(this).startOf("month").zone()-(f.zone()-bb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:h(e)},from:function(a,b){return bb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(bb(),a)},calendar:function(){var a=this.diff(bb().zone(this.zone()).startOf("day"),"days",!0),b=-6>a?"sameElse":-1>a?"lastWeek":0>a?"lastDay":1>a?"sameDay":2>a?"nextDay":7>a?"nextWeek":"sameElse";return this.format(this.lang().calendar(b,this))},isLeapYear:function(){return t(this.year())},isDST:function(){return this.zone()<this.clone().month(0).zone()||this.zone()<this.clone().month(5).zone()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=T(a,this.lang()),this.add({d:a-b})):b},month:function(a){var b,c=this._isUTC?"UTC":"";return null!=a?"string"==typeof a&&(a=this.lang().monthsParse(a),"number"!=typeof a)?this:(b=this.date(),this.date(1),this._d["set"+c+"Month"](a),this.date(Math.min(b,this.daysInMonth())),bb.updateOffset(this),this):this._d["get"+c+"Month"]()},startOf:function(a){switch(a=n(a)){case"year":this.month(0);case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),this},endOf:function(a){return a=n(a),this.startOf(a).add("isoWeek"===a?"week":a,1).subtract("ms",1)},isAfter:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)>+bb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+bb(a).startOf(b)},isSame:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)===+bb(a).startOf(b)},min:function(a){return a=bb.apply(null,arguments),this>a?this:a},max:function(a){return a=bb.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=G(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&j(this,bb.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?bb(a).zone():0,0===(this.zone()-a)%60},daysInMonth:function(){return r(this.year(),this.month())},dayOfYear:function(a){var b=eb((bb(this).startOf("day")-bb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},weekYear:function(a){var b=W(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=W(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=W(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=n(a),this[a]()},set:function(a,b){return a=n(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=A(b),this)}}),cb=0;cb<Ib.length;cb++)Z(Ib[cb].toLowerCase().replace(/s$/,""),Ib[cb]);Z("year","FullYear"),bb.fn.days=bb.fn.day,bb.fn.months=bb.fn.month,bb.fn.weeks=bb.fn.week,bb.fn.isoWeeks=bb.fn.isoWeek,bb.fn.toJSON=bb.fn.toISOString,g(bb.duration.fn=f.prototype,{_bubble:function(){var a,b,c,d,e=this._milliseconds,f=this._days,g=this._months,i=this._data;i.milliseconds=e%1e3,a=h(e/1e3),i.seconds=a%60,b=h(a/60),i.minutes=b%60,c=h(b/60),i.hours=c%24,f+=h(c/24),i.days=f%30,g+=h(f/30),i.months=g%12,d=h(g/12),i.years=d},weeks:function(){return h(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*(this._months%12)+31536e6*q(this._months/12)},humanize:function(a){var b=+this,c=V(b,!a,this.lang());return a&&(c=this.lang().pastFuture(b,c)),this.lang().postformat(c)},add:function(a,b){var c=bb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=bb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=n(a),this[a.toLowerCase()+"s"]()},as:function(a){return a=n(a),this["as"+a.charAt(0).toUpperCase()+a.slice(1)+"s"]()},lang:bb.fn.lang,toIsoString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}});for(cb in Jb)Jb.hasOwnProperty(cb)&&(_(cb,Jb[cb]),$(cb.toLowerCase()));_("Weeks",6048e5),bb.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},bb.lang("en",{ordinal:function(a){var b=a%10,c=1===q(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),nb?(module.exports=bb,ab(!0)):"function"==typeof define&&define.amd?define("moment",function(b,c,d){return d.config().noGlobal!==!0&&ab(d.config().noGlobal===a),bb}):ab()}).call(this);
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/underscore-1.4.4-min.js b/azkaban-webserver/src/web/js/underscore-1.4.4-min.js
new file mode 100644
index 0000000..c1d9d3a
--- /dev/null
+++ b/azkaban-webserver/src/web/js/underscore-1.4.4-min.js
@@ -0,0 +1 @@
+(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index<t.index?-1:1}),"value")};var F=function(n,t,r,e){var u={},i=k(t||w.identity);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};w.groupBy=function(n,t,r){return F(n,t,r,function(n,t,r){(w.has(n,t)?n[t]:n[t]=[]).push(r)})},w.countBy=function(n,t,r){return F(n,t,r,function(n,t){w.has(n,t)||(n[t]=0),n[t]++})},w.sortedIndex=function(n,t,r,e){r=null==r?w.identity:k(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);
\ No newline at end of file
build.gradle 764(+388 -376)
diff --git a/build.gradle b/build.gradle
index 6285ff4..88bc05e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,467 +3,479 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'de.obqo.gradle:gradle-lesscss-plugin:1.0-1.3.3'
classpath 'com.linkedin:gradle-dustjs-plugin:1.0.0'
+ classpath 'de.obqo.gradle:gradle-lesscss-plugin:1.0-1.3.3'
}
}
-apply plugin: 'java'
-apply plugin: 'eclipse'
-apply plugin: 'lesscss'
-apply plugin: 'dustjs'
+apply plugin: 'distribution'
-defaultTasks 'dist'
+allprojects {
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ }
+}
/**
- * Helper that calls a command and returns the output
+ * 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()
+ 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'])
-}
+subprojects {
+ apply plugin: 'java'
+ apply plugin: 'eclipse'
+
+ /**
+ * 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'
-version = getVersionName()
-archivesBaseName = 'azkaban'
-check.dependsOn.remove(test)
+ File versionFile = file('build/package/azkaban.version')
+ versionFile.parentFile.mkdirs()
+ versionFile.write(versionStr)
+ }
-repositories {
- mavenCentral()
- mavenLocal()
+ /*
+ * Print test execution summary when informational logging is enabled.
+ */
+ test {
+ testLogging {
+ afterSuite { desc, result ->
+ if (desc.getParent()) {
+ logger.info desc.getName()
+ } else {
+ logger.info "Overall"
+ }
+ logger.info " ${result.resultType} (" +
+ "${result.testCount} tests, " +
+ "${result.successfulTestCount} passed, " +
+ "${result.failedTestCount} failed, " +
+ "${result.skippedTestCount} skipped)"
+ }
+ }
+ }
}
-configurations {
+project(':azkaban-common') {
+ 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'
- }
- generateRestli {
- transitive = true
- }
- test {
- extendsFrom compile
+ transitive = false
}
+ }
+
+ dependencies {
+ compile('com.google.guava:guava:13.0.1')
+ compile('com.h2database:h2:1.3.170')
+ compile('commons-codec:commons-codec:1.9')
+ compile('commons-collections:commons-collections:3.2.1')
+ compile('commons-configuration:commons-configuration:1.8')
+ compile('commons-dbcp:commons-dbcp:1.4')
+ compile('commons-dbutils:commons-dbutils:1.5')
+ compile('commons-fileupload:commons-fileupload:1.2.1')
+ compile('commons-io:commons-io:2.4')
+ compile('commons-lang:commons-lang:2.6')
+ compile('commons-logging:commons-logging:1.1.1')
+ compile('commons-pool:commons-pool:1.6')
+ compile('javax.mail:mail:1.4.5')
+ compile('javax.servlet:servlet-api:2.5')
+ compile('joda-time:joda-time:2.0')
+ compile('log4j:log4j:1.2.16')
+ compile('mysql:mysql-connector-java:5.1.28')
+ compile('net.sf.jopt-simple:jopt-simple:4.3')
+ compile('org.apache.commons:commons-email:1.2')
+ compile('org.apache.commons:commons-jexl:2.1.1')
+ compile('org.apache.httpcomponents:httpclient:4.2.1')
+ compile('org.apache.httpcomponents:httpcore:4.2.1')
+ compile('org.apache.velocity:velocity:1.7')
+ compile('org.codehaus.jackson:jackson-core-asl:1.9.5')
+ compile('org.codehaus.jackson:jackson-mapper-asl:1.9.5')
+ compile('org.mortbay.jetty:jetty:6.1.26')
+ compile('org.mortbay.jetty:jetty-util:6.1.26')
+ compile('org.slf4j:slf4j-api:1.6.1')
+
+ testCompile('junit:junit:4.11')
+ testCompile('org.hamcrest:hamcrest-all:1.3')
+ }
}
-ext.pegasusVersion = '1.15.7'
-
-dependencies {
- compile (
- [group: 'commons-collections', name:'commons-collections', version: '3.2.1'],
- [group: 'commons-configuration', name:'commons-configuration', version: '1.8'],
- [group: 'commons-codec', name:'commons-codec', version: '1.9'],
- [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.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'],
- [group: 'com.linkedin.pegasus', name: 'gradle-plugins', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name: 'pegasus-common', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name: 'restli-common', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name: 'restli-server', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name: 'data', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name: 'r2', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name: 'li-jersey-uri', version: pegasusVersion],
- [group: 'com.linkedin.parseq', name: 'parseq', version: '1.3.7'],
- [group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.3.2']
- )
-
- generateRestli (
- [group: 'com.linkedin.pegasus', name:'generator', version: pegasusVersion],
- [group: 'com.linkedin.pegasus', name:'restli-tools', version: pegasusVersion]
- )
-
- testCompile (
- [group: 'junit', name:'junit', version: '4.11'],
- [group: 'org.hamcrest', name:'hamcrest-all', version: '1.3']
- )
+project(':azkaban-migration') {
+ configurations {
+ all {
+ transitive = false
+ }
+ }
+
+ dependencies {
+ compile(project(':azkaban-common'))
+ compile('commons-io:commons-io:2.4')
+ compile('commons-dbcp:commons-dbcp:1.4')
+ compile('commons-dbutils:commons-dbutils:1.5')
+ compile('joda-time:joda-time:2.0')
+ compile('log4j:log4j:1.2.16')
+ }
+
+ task copyLibs(type: Copy, dependsOn: 'build') {
+ from('build/libs')
+ into('build/package/lib')
+ }
+
+ task copyDeps(type: Copy, dependsOn: 'build') {
+ from(project(':azkaban-common').configurations.compile)
+ from(configurations.compile)
+ into('build/package/lib')
+ }
+
+ task copyPackage(type: Copy) {
+ from('src/package')
+ into('build/package')
+ }
+
+ task copy(dependsOn: [
+ 'createVersionFile',
+ 'copyLibs',
+ 'copyDeps',
+ 'copyPackage']) {
+ }
}
-sourceSets {
- main {
- java {
- srcDirs 'src/main/java', 'src/restli/generatedJava', 'src/restli/java'
- }
+project(':azkaban-webserver') {
+ apply plugin: 'lesscss'
+ apply plugin: 'dustjs'
+
+ ext.pegasusVersion = '1.15.7'
+
+ configurations {
+ all {
+ transitive = false
}
- test {
- java {
- srcDirs 'unit/java'
- }
+ generateRestli {
+ transitive = true
}
-}
+ }
+
+ dependencies {
+ compile(project(':azkaban-common'))
+ compile('com.linkedin.parseq:parseq:1.3.7')
+ compile('com.linkedin.pegasus:data:' + pegasusVersion)
+ compile('com.linkedin.pegasus:gradle-plugins:' + pegasusVersion)
+ compile('com.linkedin.pegasus:li-jersey-uri:' + pegasusVersion)
+ compile('com.linkedin.pegasus:pegasus-common:' + pegasusVersion)
+ compile('com.linkedin.pegasus:restli-common:' + pegasusVersion)
+ compile('com.linkedin.pegasus:restli-server:' + pegasusVersion)
+ compile('com.linkedin.pegasus:r2:' + pegasusVersion)
+ compile('com.fasterxml.jackson.core:jackson-core:2.3.2')
+ compile('commons-lang:commons-lang:2.6')
+ compile('commons-io:commons-io:2.4')
+ compile('commons-fileupload:commons-fileupload:1.2.1')
+ compile('javax.servlet:servlet-api:2.5')
+ compile('joda-time:joda-time:2.0')
+ compile('log4j:log4j:1.2.16')
+ compile('net.sf.jopt-simple:jopt-simple:4.3')
+ compile('org.apache.velocity:velocity:1.7')
+ compile('org.mortbay.jetty:jetty:6.1.26')
+ compile('org.mortbay.jetty:jetty-util:6.1.26')
+
+ generateRestli('com.linkedin.pegasus:generator:' + pegasusVersion)
+ generateRestli('com.linkedin.pegasus:restli-tools:' + pegasusVersion)
+ }
-jar {
- baseName = 'azkaban'
- manifest {
- attributes(
- 'Implementation-Title': 'Azkaban',
- 'Implementation-Version': version
- )
+ sourceSets {
+ main {
+ java {
+ srcDirs 'src/main/java', 'src/restli/generatedJava', 'src/restli/java'
+ }
}
-}
+ }
-task restliTemplateGenerator(type: JavaExec) {
+ task restliTemplateGenerator(type: JavaExec) {
mkdir 'src/restli/generatedJava'
main = 'com.linkedin.pegasus.generator.PegasusDataTemplateGenerator'
args = ['src/restli/generatedJava','src/restli/schemas']
classpath = configurations.generateRestli
-}
+ }
-task restliRestSpecGenerator(dependsOn: [restliTemplateGenerator], type: JavaExec) << {
+ task restliRestSpecGenerator(dependsOn: [restliTemplateGenerator], type: JavaExec) << {
mkdir 'src/restli/generatedRestSpec'
main = 'com.linkedin.restli.tools.idlgen.RestLiResourceModelExporterCmdLineApp'
args = ['-outdir', 'src/restli/generatedRestSpec', '-sourcepath', 'src/restli/java']
classpath = configurations.generateRestli
-}
+ }
-task restli(dependsOn: restliTemplateGenerator) << {
-}
+ task restli(dependsOn: restliTemplateGenerator) << {
+ }
-compileJava.dependsOn.add('restli')
+ compileJava.dependsOn.add('restli')
-eclipse.classpath.file {
- // Erase the whole classpath
- beforeMerged {
- classpath -> classpath.entries.removeAll { entry -> true }
+ lesscss {
+ source = fileTree('src/main/less') {
+ include 'azkaban.less'
+ include 'azkaban-graph.less'
}
+ dest = 'build/less'
+ }
- // 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 = []
- }
-}
+ dustjs {
+ source = fileTree('src/main/tl')
+ dest = 'build/dust'
+ }
-lesscss {
- source = fileTree('src/main/less') {
- include 'azkaban.less'
- include 'azkaban-graph.less'
+ task createDirs() << {
+ file('build/package/extlib').mkdirs()
+ file('build/package/plugins').mkdirs()
}
- dest = 'build/web/css'
-}
-dustjs {
- source = fileTree('src/main/tl')
- dest = 'build/web/js'
-}
+ task copyWeb(type: Copy) {
+ from('src/web')
+ into('build/package/web')
+ }
-/**
- * Copies web files to a build directory
- */
-task web(dependsOn: ['lesscss', 'dustjs']) << {
- println 'Copying web files'
- copy {
- from('src/web')
- into('build/web')
- }
-}
+ task copyLess(type: Copy, dependsOn: ['lesscss', 'copyWeb']) {
+ from('build/less')
+ into('build/package/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')
+ task copyDust(type: Copy, dependsOn: ['dustjs', 'copyWeb']) {
+ from('build/dust')
+ into('build/package/web/js')
+ }
- String versionStr = version + '\n' +
- gitCommitHash + '\n' +
- gitRepo + '\n' +
- formattedDate + '\n'
+ task copyDeps(type: Copy, dependsOn: 'build') {
+ from(project(':azkaban-common').configurations.compile)
+ from(configurations.compile)
+ into('build/package/lib')
+ }
- File versionFile = file('build/package/version.file')
- versionFile.parentFile.mkdirs()
- versionFile.write(versionStr)
-}
+ task copyLibs(type: Copy, dependsOn: 'build') {
+ from('build/libs')
+ into('build/package/lib')
+ }
-ext.soloAppendix = 'solo-server'
-ext.soloPackageDir = 'build/package/' + jar.baseName + '-' + soloAppendix
+ task copyPackage(type: Copy) {
+ from('src/package')
+ into('build/package')
+ }
-/**
- * Copies the Azkaban Solo Server files into its package directory.
- */
-task copySolo(dependsOn: ['jar', 'web', 'createVersionFile']) << {
- delete soloPackageDir
- mkdir soloPackageDir
-
- println 'Creating Azkaban Solo Server Package into ' + soloPackageDir
- mkdir soloPackageDir + '/lib'
- mkdir soloPackageDir + '/extlib'
- mkdir soloPackageDir + '/plugins'
-
- copy {
- println 'Copying Soloserver bin & conf'
- from('src/package/soloserver')
- into(soloPackageDir)
- }
+ task copy(dependsOn: [
+ 'createVersionFile',
+ 'createDirs',
+ 'copyLess',
+ 'copyDust',
+ 'copyDeps',
+ 'copyLibs',
+ 'copyPackage']) {
+ }
+}
- copy {
- println 'Copying Azkaban lib'
- from('build/libs')
- into(soloPackageDir + '/lib')
+project(':azkaban-execserver') {
+ configurations {
+ all {
+ transitive = false
}
+ }
- copy {
- println 'Copying web'
- from('build/web')
- into(soloPackageDir + '/web')
- }
+ dependencies {
+ compile(project(':azkaban-common'))
+ compile('commons-io:commons-io:2.4')
+ compile('javax.servlet:servlet-api:2.5')
+ compile('joda-time:joda-time:2.0')
+ compile('log4j:log4j:1.2.16')
+ compile('org.mortbay.jetty:jetty:6.1.26')
+ compile('org.mortbay.jetty:jetty-util:6.1.26')
+ compile('org.codehaus.jackson:jackson-core-asl:1.9.5')
+ compile('org.codehaus.jackson:jackson-mapper-asl:1.9.5')
+
+ testCompile('junit:junit:4.11')
+ testCompile('org.hamcrest:hamcrest-all:1.3')
+ testCompile(project(':azkaban-common').sourceSets.test.output)
+ }
- copy {
- println 'Copying sql'
- from('src/sql')
- into(soloPackageDir + '/sql')
- }
+ task createDirs() << {
+ file('build/package/extlib').mkdirs()
+ file('build/package/plugins').mkdirs()
+ }
- copy {
- println 'Copying dependency jars'
- into soloPackageDir + '/lib'
- from configurations.compile
- }
+ task copyDeps(type: Copy, dependsOn: 'build') {
+ from(project(':azkaban-common').configurations.compile)
+ from(configurations.compile)
+ into('build/package/lib')
+ }
- copy {
- println 'Copying version file'
- into soloPackageDir
- from 'build/package/version.file'
- }
-}
+ task copyLibs(type: Copy, dependsOn: 'build') {
+ from('build/libs')
+ into('build/package/lib')
+ }
-/**
- * Packages the SoloServer version of Azkaban
- */
-task packageSolo(type: Tar, dependsOn: 'copySolo') {
- appendix = soloAppendix
- extension = 'tar.gz'
- compression = Compression.GZIP
-
- ext.basedir = baseName + '-' + appendix + '-' + version
- into(basedir) {
- from soloPackageDir
- exclude 'bin'
- }
+ task copyPackage(type: Copy) {
+ from('src/package')
+ into('build/package')
+ }
- ext.dst_bin = basedir + '/bin'
- ext.src_bin = soloPackageDir + '/bin'
- from(src_bin) {
- into dst_bin
- fileMode = 0755
- }
+ task copy(dependsOn: [
+ 'createVersionFile',
+ 'createDirs',
+ 'copyDeps',
+ 'copyLibs',
+ 'copyPackage']) {
+ }
}
-ext.sqlPackageDir = 'build/package/sql'
-
-/**
- * Copies the SQL files into its package directory.
- */
-task copySql() << {
- println 'Creating Azkaban SQL Scripts into ' + sqlPackageDir
- delete sqlPackageDir
- mkdir sqlPackageDir
-
- copy {
- println 'Copying SQL files'
- from('src/sql')
- into(sqlPackageDir)
- }
+project(':azkaban-soloserver') {
+ dependencies {
+ compile(project(':azkaban-common'))
+ compile(project(':azkaban-webserver'))
+ compile(project(':azkaban-execserver'))
+ }
- String destFile = sqlPackageDir + '/create-all-sql-' + version + '.sql';
- ant.concat(destfile:destFile, fixlastline:'yes') {
- println('Concating create scripts to ' + destFile)
- fileset(dir: 'src/sql') {
- exclude(name: 'update.*.sql')
- exclude(name: 'database.properties')
- }
- }
-}
+ task createDirs() << {
+ file('build/package/extlib').mkdirs()
+ file('build/package/plugins').mkdirs()
+ }
-/**
- * Packages the Sql Scripts for Azkaban DB
- */
-task packageSql(type: Tar, dependsOn: 'copySql') {
- extension = 'tar.gz'
- compression = Compression.GZIP
- appendix = 'sql'
-
- ext.basedir = baseName + '-' + appendix + '-' + version
- into(basedir) {
- from sqlPackageDir
- }
-}
+ task copyDeps(type: Copy, dependsOn: 'build') {
+ from(configurations.compile)
+ into('build/package/lib')
+ }
-ext.execAppendix = 'exec-server'
-ext.execPackageDir = 'build/package/' + jar.baseName + '-' + execAppendix
+ task copyLibs(type: Copy, dependsOn: 'build') {
+ from('build/libs')
+ into('build/package/lib')
+ }
-/**
- * Copies the Azkaban Executor Server files into its package directory.
- */
-task copyExec(dependsOn: ['jar', 'createVersionFile']) << {
- delete execPackageDir
- println 'Creating Azkaban Executor Server Package into ' + execPackageDir
- mkdir execPackageDir
- mkdir execPackageDir + '/lib'
- mkdir execPackageDir + '/extlib'
- mkdir execPackageDir + '/plugins'
-
- copy {
- println 'Copying Exec server bin & conf'
- from('src/package/execserver')
- into(execPackageDir)
- }
+ task copyPackage(type: Copy) {
+ from('src/package')
+ into('build/package')
+ }
- copy {
- println 'Copying Azkaban lib '
- from('build/libs')
- into(execPackageDir + '/lib')
- }
+ task copyWeb(type: Copy, dependsOn: ':azkaban-webserver:copy') {
+ from(project(':azkaban-webserver').files('build/package/web'))
+ into('build/package/web')
+ }
- copy {
- println 'Copying dependency jars'
- into execPackageDir + '/lib'
- from configurations.compile
- }
+ task copySql(type: Copy) {
+ from(project(':azkaban-sql').files('src/sql'))
+ into('build/package/sql')
+ }
- copy {
- into execPackageDir
- from 'build/package/version.file'
- }
+ task copy(dependsOn: [
+ 'createVersionFile',
+ 'createDirs',
+ 'copyDeps',
+ 'copyLibs',
+ 'copyPackage',
+ 'copyWeb',
+ 'copySql']) {
+ }
}
-/**
- * Packages the Azkaban Executor Server
- */
-task packageExec(type: Tar, dependsOn: 'copyExec') {
- appendix = execAppendix
- extension = 'tar.gz'
- compression = Compression.GZIP
-
- ext.basedir = baseName + '-' + appendix + '-' + version
- into(basedir) {
- from execPackageDir
- exclude 'bin'
- }
-
- ext.dst_bin = basedir + '/bin'
- ext.src_bin = execPackageDir + '/bin'
- from(src_bin) {
- into dst_bin
- fileMode = 0755
+project(':azkaban-sql') {
+ task concat() << {
+ ext.destFile = 'build/sql/create-all-sql-' + version + '.sql';
+ ant.concat(destfile: destFile, fixlastline: 'yes') {
+ logger.info('Concating create scripts to ' + destFile)
+ fileset(dir: 'src/sql') {
+ exclude(name: 'update.*.sql')
+ exclude(name: 'database.properties')
+ }
}
+ }
}
-ext.webAppendix = 'web-server'
-ext.webPackageDir = 'build/package/' + jar.baseName + '-' + webAppendix
-
-/**
- * Copies the Azkaban Web Server files into its package directory.
- */
-task copyWeb(dependsOn: ['jar', 'web', 'createVersionFile']) << {
- println 'Creating Azkaban Web Server Package into ' + webPackageDir
- delete webPackageDir
- mkdir webPackageDir
- mkdir webPackageDir + '/lib'
- mkdir webPackageDir + '/extlib'
- mkdir webPackageDir + '/plugins'
-
- println 'Copying Web server bin & conf'
- copy {
- from('src/package/webserver')
- into(webPackageDir)
+distributions {
+ migration {
+ baseName = 'azkaban-migration'
+ contents {
+ from { project(':azkaban-migration').file('build/package') }
}
+ }
- println 'Copying Azkaban lib '
- copy {
- from('build/libs')
- into(webPackageDir + '/lib')
+ webserver {
+ baseName = 'azkaban-web-server'
+ contents {
+ from { project(':azkaban-webserver').file('build/package') }
}
+ }
- println 'Copying web'
- copy {
- from('build/web')
- into(webPackageDir + '/web')
+ execserver {
+ baseName = 'azkaban-exec-server'
+ contents {
+ from { project(':azkaban-execserver').file('build/package') }
}
+ }
- println 'Copying dependency jars'
- copy {
- into webPackageDir + '/lib'
- from configurations.compile
+ soloserver {
+ baseName = 'azkaban-solo-server'
+ contents {
+ from { project(':azkaban-soloserver').files('build/package') }
}
+ }
- copy {
- into webPackageDir
- from 'build/package/version.file'
+ sql {
+ baseName = 'azkaban-sql'
+ contents {
+ from { project(':azkaban-sql').file('src/sql') }
+ from { project(':azkaban-sql').file('build/sql') }
}
+ }
}
+// Set up dependencies for distribution tasks.
+//
+// N.B. The extension for the Tar tasks is set since the Gradle Distribution
+// plugin uses the .tar file extension for GZipped Tar files by default.
+//
+// N.B. When the distribution tasks are run, azkaban-execserver,
+// azkaban-webserver, azkaban-migration, and azkaban-soloserver only
+// have a dependency on the azkaban-common build artifacts. As a result,
+// the full :azkaban-common:build task is not run, meaning that the
+// tests are skipped. Thus, the dependency on :azkaban-common:build
+// is set here so that the azkaban-common unit tests are run when running
+// the dist tasks.
+
+migrationDistTar.dependsOn ':azkaban-common:build', ':azkaban-migration:copy'
+migrationDistTar.extension = 'tar.gz'
+migrationDistZip.dependsOn ':azkaban-common:build', ':azkaban-migration:copy'
+
+webserverDistTar.dependsOn ':azkaban-common:build', ':azkaban-webserver:copy'
+webserverDistTar.extension = 'tar.gz'
+webserverDistZip.dependsOn ':azkaban-common:build', ':azkaban-webserver:copy'
+
+execserverDistTar.dependsOn ':azkaban-common:build', ':azkaban-execserver:copy'
+execserverDistTar.extension = 'tar.gz'
+execserverDistZip.dependsOn ':azkaban-common:build', ':azkaban-execserver:copy'
+
+soloserverDistTar.dependsOn ':azkaban-common:build', ':azkaban-soloserver:copy'
+soloserverDistTar.extension = 'tar.gz'
+soloserverDistZip.dependsOn ':azkaban-common:build', ':azkaban-soloserver:copy'
+
+sqlDistTar.dependsOn ':azkaban-sql:concat'
+sqlDistTar.extension = 'tar.gz'
+sqlDistZip.dependsOn ':azkaban-sql:concat'
+
+distTar.dependsOn migrationDistTar, webserverDistTar, execserverDistTar, soloserverDistTar, sqlDistTar
+distZip.dependsOn migrationDistZip, webserverDistZip, execserverDistZip, soloserverDistZip, sqlDistZip
+
/**
- * Packages the Azkaban Web Server
+ * Gradle wrapper task.
*/
-task packageWeb(type: Tar, dependsOn: 'copyWeb') {
- appendix = webAppendix
- extension = 'tar.gz'
- compression = Compression.GZIP
-
- ext.basedir = baseName + '-' + appendix + '-' + version
- into(basedir) {
- from webPackageDir
- exclude 'bin'
- }
-
- ext.dst_bin = basedir + '/bin'
- ext.src_bin = webPackageDir + '/bin'
- from(src_bin) {
- into dst_bin
- fileMode = 0755
- }
-}
-
-task packageAll(dependsOn: ['packageWeb',
- 'packageExec',
- 'packageSolo',
- 'packageSql']) {
-}
-
-task dist(dependsOn: 'packageAll') {
-}
-
task wrapper(type: Wrapper) {
- gradleVersion = '1.11'
+ gradleVersion = '1.12'
}
gradle.properties 2(+2 -0)
diff --git a/gradle.properties b/gradle.properties
index 1a644c7..40f5cae 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1,3 @@
org.gradle.daemon=true
+group=com.linkedin
+version=2.6.0-SNAPSHOT
gradle/wrapper/gradle-wrapper.jar 0(+0 -0)
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 3c7abdf..0087cd3 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2d93fc4..506745b 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu Mar 20 14:12:56 PDT 2014
+#Wed Jun 11 01:55:01 PDT 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-1.12-bin.zip
README.md 27(+22 -5)
diff --git a/README.md b/README.md
index b55096d..ba4ed4b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,24 @@
-## Azkaban2
+Azkaban2 [](https://travis-ci.org/azkaban/azkaban2)
+========
-[](https://travis-ci.org/azkaban/azkaban2)
+Building from Source
+--------------------
-For Azkaban documentation, please go to
-[Azkaban Project Site](http://azkaban.github.io/azkaban2/)
-There is a google groups: [Azkaban Group](https://groups.google.com/forum/?fromgroups#!forum/azkaban-dev)
+To build Azkaban packages from source, run:
+
+```
+./gradlew distTar
+```
+
+The above command builds all Azkaban packages and packages them into GZipped Tar archives. To build Zip archives, run:
+
+```
+./gradlew distZip
+```
+
+Documentation
+-------------
+
+For Azkaban documentation, please go to [Azkaban Project Site](http://azkaban.github.io)
+
+For help, please visit the Azkaban Google Group: [Azkaban Group](https://groups.google.com/forum/?fromgroups#!forum/azkaban-dev)
settings.gradle 6(+6 -0)
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..dcfd8d0
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,6 @@
+include 'azkaban-common'
+include 'azkaban-execserver'
+include 'azkaban-migration'
+include 'azkaban-soloserver'
+include 'azkaban-sql'
+include 'azkaban-webserver'