azkaban-aplcache

Final commit for Flexible Scheduling This commit resolved:

9/12/2016 4:57:13 PM

Changes

Details

diff --git a/azkaban-common/build.gradle b/azkaban-common/build.gradle
index 6a24945..78c04ce 100644
--- a/azkaban-common/build.gradle
+++ b/azkaban-common/build.gradle
@@ -53,6 +53,7 @@ dependencies {
   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')
+  compile('org.quartz-scheduler:quartz:2.2.1')
 
   testCompile(project(':azkaban-test').sourceSets.test.output)
   testCompile('junit:junit:4.11')
diff --git a/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java b/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java
index 2b2fc88..27d3f30 100644
--- a/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java
+++ b/azkaban-common/src/main/java/azkaban/scheduler/Schedule.java
@@ -16,7 +16,13 @@
 
 package azkaban.scheduler;
 
+import azkaban.executor.ExecutionOptions;
+import azkaban.sla.SlaOption;
+import azkaban.utils.Pair;
+import azkaban.utils.Utils;
+
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -31,10 +37,7 @@ import org.joda.time.Months;
 import org.joda.time.ReadablePeriod;
 import org.joda.time.Seconds;
 import org.joda.time.Weeks;
-
-import azkaban.executor.ExecutionOptions;
-import azkaban.sla.SlaOption;
-import azkaban.utils.Pair;
+import org.quartz.CronExpression;
 
 public class Schedule {
 
@@ -50,6 +53,7 @@ public class Schedule {
   private String submitUser;
   private String status;
   private long submitTime;
+  private String cronExpression;
 
   private boolean skipPastOccurrences = true;
 
@@ -63,7 +67,7 @@ public class Schedule {
 
     this(scheduleId, projectId, projectName, flowName, status, firstSchedTime,
         timezone, period, lastModifyTime, nextExecTime, submitTime, submitUser,
-        null, null);
+        null, null, null);
   }
 
   public Schedule(int scheduleId, int projectId, String projectName,
@@ -74,14 +78,14 @@ public class Schedule {
     this(scheduleId, projectId, projectName, flowName, status, firstSchedTime,
         DateTimeZone.forID(timezoneId), parsePeriodString(period),
         lastModifyTime, nextExecTime, submitTime, submitUser, executionOptions,
-        slaOptions);
+        slaOptions, null);
   }
 
   public Schedule(int scheduleId, int projectId, String projectName,
       String flowName, String status, long firstSchedTime,
       DateTimeZone timezone, ReadablePeriod period, long lastModifyTime,
       long nextExecTime, long submitTime, String submitUser,
-      ExecutionOptions executionOptions, List<SlaOption> slaOptions) {
+      ExecutionOptions executionOptions, List<SlaOption> slaOptions, String cronExpression) {
     this.scheduleId = scheduleId;
     this.projectId = projectId;
     this.projectName = projectName;
@@ -96,6 +100,7 @@ public class Schedule {
     this.submitTime = submitTime;
     this.executionOptions = executionOptions;
     this.slaOptions = slaOptions;
+    this.cronExpression = cronExpression;
   }
 
   public ExecutionOptions getExecutionOptions() {
@@ -119,11 +124,16 @@ public class Schedule {
   }
 
   public String toString() {
-    return projectName + "." + flowName + " (" + projectId + ")"
-        + " to be run at (starting) "
-        + new DateTime(firstSchedTime).toDateTimeISO()
-        + " with recurring period of "
-        + (period == null ? "non-recurring" : createPeriodString(period));
+
+    String underlying = projectName + "." + flowName + " (" + projectId + ")" + " to be run at (starting) " + new DateTime(
+        firstSchedTime).toDateTimeISO();
+    if (period == null && cronExpression == null) {
+      return underlying + " non-recurring";
+    } else if (cronExpression != null) {
+      return underlying + " with CronExpression {" + cronExpression + "}";
+    } else {
+      return underlying + " with precurring period of " + createPeriodString(period);
+    }
   }
 
   public Pair<Integer, String> getScheduleIdentityPair() {
@@ -182,11 +192,21 @@ public class Schedule {
     return submitTime;
   }
 
+  public String getCronExpression() {
+    return cronExpression;
+  }
+
   public boolean updateTime() {
     if (new DateTime(nextExecTime).isAfterNow()) {
       return true;
     }
 
+    if (cronExpression != null) {
+      DateTime nextTime = getNextCronRuntime(nextExecTime, timezone, Utils.parseCronExpression(cronExpression, timezone));
+      this.nextExecTime = nextTime.getMillis();
+      return true;
+    }
+
     if (period != null) {
       DateTime nextTime = getNextRuntime(nextExecTime, timezone, period);
 
@@ -224,6 +244,23 @@ public class Schedule {
     return date;
   }
 
+  /**
+   *
+   * @param scheduleTime represents the time when Schedule Servlet receives the Cron Schedule API call.
+   * @param timezone is always UTC (after 3.1.0)
+   * @param ce
+   * @return the First Scheduled DateTime to run this flow.
+   */
+  private DateTime getNextCronRuntime(long scheduleTime, DateTimeZone timezone,
+      CronExpression ce) {
+
+    Date date = new DateTime(scheduleTime).withZone(timezone).toDate();
+    if (ce != null) {
+      date = ce.getNextValidTimeAfter(date);
+    }
+    return new DateTime(date);
+  }
+
   public static ReadablePeriod parsePeriodString(String periodStr) {
     ReadablePeriod period;
     char periodUnit = periodStr.charAt(periodStr.length() - 1);
@@ -341,7 +378,7 @@ public class Schedule {
   }
 
   public boolean isRecurring() {
-    return period == null ? false : true;
+    return period != null || cronExpression != null;
   }
 
   public boolean skipPastOccurrences() {
diff --git a/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java b/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java
index c2489ec..fbfef13 100644
--- a/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java
+++ b/azkaban-common/src/main/java/azkaban/scheduler/ScheduleManager.java
@@ -204,7 +204,7 @@ public class ScheduleManager implements TriggerAgent {
     Schedule sched =
         new Schedule(scheduleId, projectId, projectName, flowName, status,
             firstSchedTime, timezone, period, lastModifyTime, nextExecTime,
-            submitTime, submitUser, execOptions, slaOptions);
+            submitTime, submitUser, execOptions, slaOptions, null);
     logger
         .info("Scheduling flow '" + sched.getScheduleName() + "' for "
             + _dateFormat.print(firstSchedTime) + " with a period of " + period == null ? "(non-recurring)"
@@ -214,6 +214,23 @@ public class ScheduleManager implements TriggerAgent {
     return sched;
   }
 
+  public Schedule cronScheduleFlow(final int scheduleId, final int projectId,
+      final String projectName, final String flowName, final String status,
+      final long firstSchedTime, final DateTimeZone timezone,
+      final long lastModifyTime,
+      final long nextExecTime, final long submitTime, final String submitUser,
+      ExecutionOptions execOptions, List<SlaOption> slaOptions, String cronExpression) {
+    Schedule sched =
+        new Schedule(scheduleId, projectId, projectName, flowName, status,
+            firstSchedTime, timezone, null, lastModifyTime, nextExecTime,
+            submitTime, submitUser, execOptions, slaOptions, cronExpression);
+    logger
+        .info("Scheduling flow '" + sched.getScheduleName() + "' for "
+            + _dateFormat.print(firstSchedTime) + " cron Expression = " + cronExpression);
+
+    insertSchedule(sched);
+    return sched;
+  }
   /**
    * Schedules the flow, but doesn't save the schedule afterwards.
    *
diff --git a/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java b/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java
index 7a3c12a..e1bcf81 100644
--- a/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java
+++ b/azkaban-common/src/main/java/azkaban/scheduler/TriggerBasedScheduleLoader.java
@@ -83,7 +83,7 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
     ConditionChecker checker =
         new BasicTimeChecker("BasicTimeChecker_1", s.getFirstSchedTime(),
             s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
-            s.getPeriod());
+            s.getPeriod(), s.getCronExpression());
     checkers.put(checker.getId(), checker);
     String expr = checker.getId() + ".eval()";
     Condition cond = new Condition(checkers, expr);
@@ -97,7 +97,7 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
     ConditionChecker checker =
         new BasicTimeChecker("BasicTimeChecker_2", s.getFirstSchedTime(),
             s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
-            s.getPeriod());
+            s.getPeriod(), s.getCronExpression());
     checkers.put(checker.getId(), checker);
     String expr = checker.getId() + ".eval()";
     Condition cond = new Condition(checkers, expr);
@@ -135,8 +135,8 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
       lastUpdateTime = Math.max(lastUpdateTime, t.getLastModifyTime());
       Schedule s = triggerToSchedule(t);
       schedules.add(s);
-      System.out.println("loaded schedule for " + s.getProjectId()
-          + s.getProjectName());
+      System.out.println("loaded schedule for "
+          + s.getProjectName() + " (project_ID: " + s.getProjectId() + ")");
     }
     return schedules;
 
@@ -167,7 +167,7 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
               t.getStatus().toString(), ck.getFirstCheckTime(),
               ck.getTimeZone(), ck.getPeriod(), t.getLastModifyTime(),
               ck.getNextCheckTime(), t.getSubmitTime(), t.getSubmitUser(),
-              act.getExecutionOptions(), act.getSlaOptions());
+              act.getExecutionOptions(), act.getSlaOptions(), ck.getCronExpression());
       return s;
     } else {
       logger.error("Failed to parse schedule from trigger!");
@@ -207,8 +207,8 @@ public class TriggerBasedScheduleLoader implements ScheduleLoader {
       lastUpdateTime = Math.max(lastUpdateTime, t.getLastModifyTime());
       Schedule s = triggerToSchedule(t);
       schedules.add(s);
-      System.out.println("loaded schedule for " + s.getProjectId()
-          + s.getProjectName());
+      System.out.println("loaded schedule for "
+          + s.getProjectName() + " (project_ID: " + s.getProjectId() + ")");
     }
     return schedules;
   }
diff --git a/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java b/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java
index 4731dde..52ff2e0 100644
--- a/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java
+++ b/azkaban-common/src/main/java/azkaban/trigger/builtin/BasicTimeChecker.java
@@ -18,16 +18,22 @@ package azkaban.trigger.builtin;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Date;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.ReadablePeriod;
+import org.apache.log4j.Logger;
+
+import org.quartz.CronExpression;
 
 import azkaban.trigger.ConditionChecker;
 import azkaban.utils.Utils;
 
 public class BasicTimeChecker implements ConditionChecker {
 
+  private static Logger logger = Logger.getLogger(BasicTimeChecker.class);
+
   public static final String type = "BasicTimeChecker";
 
   private long firstCheckTime;
@@ -37,11 +43,13 @@ public class BasicTimeChecker implements ConditionChecker {
   private boolean skipPastChecks = true;
   private ReadablePeriod period;
 
+  private String cronExpression;
+  private CronExpression cronExecutionTime;
   private final String id;
 
   public BasicTimeChecker(String id, long firstCheckTime,
       DateTimeZone timezone, boolean isRecurring, boolean skipPastChecks,
-      ReadablePeriod period) {
+      ReadablePeriod period, String cronExpression) {
     this.id = id;
     this.firstCheckTime = firstCheckTime;
     this.timezone = timezone;
@@ -49,6 +57,8 @@ public class BasicTimeChecker implements ConditionChecker {
     this.skipPastChecks = skipPastChecks;
     this.period = period;
     this.nextCheckTime = firstCheckTime;
+    this.cronExpression = cronExpression;
+    cronExecutionTime = Utils.parseCronExpression(cronExpression, timezone);
     this.nextCheckTime = calculateNextCheckTime();
   }
 
@@ -76,9 +86,13 @@ public class BasicTimeChecker implements ConditionChecker {
     return nextCheckTime;
   }
 
+  public String getCronExpression() {
+    return cronExpression;
+  }
+
   public BasicTimeChecker(String id, long firstCheckTime,
       DateTimeZone timezone, long nextCheckTime, boolean isRecurring,
-      boolean skipPastChecks, ReadablePeriod period) {
+      boolean skipPastChecks, ReadablePeriod period, String cronExpression) {
     this.id = id;
     this.firstCheckTime = firstCheckTime;
     this.timezone = timezone;
@@ -86,6 +100,8 @@ public class BasicTimeChecker implements ConditionChecker {
     this.isRecurring = isRecurring;
     this.skipPastChecks = skipPastChecks;
     this.period = period;
+    this.cronExpression = cronExpression;
+    cronExecutionTime = Utils.parseCronExpression(cronExpression, timezone);
   }
 
   @Override
@@ -130,10 +146,11 @@ public class BasicTimeChecker implements ConditionChecker {
     ReadablePeriod period =
         Utils.parsePeriodString((String) jsonObj.get("period"));
     String id = (String) jsonObj.get("id");
+    String cronExpression = (String) jsonObj.get("cronExpression");
 
     BasicTimeChecker checker =
         new BasicTimeChecker(id, firstCheckTime, timezone, nextCheckTime,
-            isRecurring, skipPastChecks, period);
+            isRecurring, skipPastChecks, period, cronExpression);
     if (skipPastChecks) {
       checker.updateNextCheckTime();
     }
@@ -157,8 +174,11 @@ public class BasicTimeChecker implements ConditionChecker {
         throw new IllegalStateException(
             "100000 increments of period did not get to present time.");
       }
-      if (period == null) {
+      if (period == null && cronExpression == null) {
         break;
+      } else if (cronExecutionTime != null) {
+        Date nextDate = cronExecutionTime.getNextValidTimeAfter(date.toDate());
+        date = new DateTime(nextDate);
       } else {
         date = date.plus(period);
       }
@@ -186,6 +206,7 @@ public class BasicTimeChecker implements ConditionChecker {
     jsonObj.put("skipPastChecks", String.valueOf(skipPastChecks));
     jsonObj.put("period", Utils.createPeriodString(period));
     jsonObj.put("id", id);
+    jsonObj.put("cronExpression", cronExpression);
 
     return jsonObj;
   }
diff --git a/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java b/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java
index e825202..89f4622 100644
--- a/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java
+++ b/azkaban-common/src/main/java/azkaban/trigger/TriggerManager.java
@@ -60,7 +60,7 @@ public class TriggerManager extends EventHandler implements
   private final Object syncObj = new Object();
 
   private String scannerStage = "";
-  
+
   public TriggerManager(Props props, TriggerLoader triggerLoader,
       ExecutorManager executorManager) throws TriggerManagerException {
 
@@ -277,6 +277,7 @@ public class TriggerManager extends EventHandler implements
             shouldSkip = false;
           }
 
+          logger.info("Get Next Check Time =" + t.getNextCheckTime() + "  now = " + now );
           if (shouldSkip) {
             logger.info("Skipping trigger" + t.getTriggerId() + " until " + t.getNextCheckTime());
           }
diff --git a/azkaban-common/src/main/java/azkaban/utils/Utils.java b/azkaban-common/src/main/java/azkaban/utils/Utils.java
index 9cdeaaf..ca53c76 100644
--- a/azkaban-common/src/main/java/azkaban/utils/Utils.java
+++ b/azkaban-common/src/main/java/azkaban/utils/Utils.java
@@ -30,11 +30,17 @@ import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.Enumeration;
 import java.util.Random;
+import java.util.TimeZone;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
+import java.text.ParseException;
 
 import org.apache.commons.io.IOUtils;
+
+import org.apache.log4j.Logger;
+
+import org.joda.time.DateTimeZone;
 import org.joda.time.DateTime;
 import org.joda.time.Days;
 import org.joda.time.DurationFieldType;
@@ -46,10 +52,15 @@ import org.joda.time.Seconds;
 import org.joda.time.Weeks;
 import org.joda.time.Years;
 
+import org.quartz.CronExpression;
+
 /**
  * A util helper class full of static methods that are commonly used.
  */
 public class Utils {
+
+  private static Logger logger = Logger
+      .getLogger(Utils.class);
   public static final Random RANDOM = new Random();
 
   /**
@@ -455,4 +466,30 @@ public class Utils {
 
     return sizeInKb;
   }
+
+  /**
+   * @param cronExpression: A cron expression is a string separated by white space, to provide a parser and evaluator for Quartz cron expressions.
+   * @return : org.quartz.CronExpression object.
+   *
+   * TODO: Currently, we have to transform Joda Timezone to Java Timezone due to CronExpression.
+   *       Since Java8 enhanced Time functionalities, We consider transform all Jodatime to Java Time in future.
+   *
+   */
+  public static CronExpression parseCronExpression(String cronExpression, DateTimeZone timezone) {
+    if (cronExpression != null) {
+      try {
+        CronExpression ce =  new CronExpression(cronExpression);
+        ce.setTimeZone(TimeZone.getTimeZone(timezone.getID()));
+        return ce;
+      } catch (ParseException pe) {
+        logger.error("this cron expression {" + cronExpression + "} can not be parsed. "
+            + "Please Check Quartz Cron Syntax.");
+      }
+      return null;
+    } else return null;
+  }
+
+  public static boolean isCronExpressionValid(String cronExpression) {
+    return CronExpression.isValidExpression(cronExpression);
+  }
 }
diff --git a/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java b/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
index 926f705..38b0f4d 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/BasicTimeCheckerTest.java
@@ -20,6 +20,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
 import org.joda.time.ReadablePeriod;
 
 import org.junit.Test;
@@ -32,11 +33,18 @@ import azkaban.trigger.builtin.BasicTimeChecker;
 
 public class BasicTimeCheckerTest {
 
-  @Test
-  public void basicTimerTest() {
 
+  private Condition getCondition(BasicTimeChecker timeChecker){
     Map<String, ConditionChecker> checkers =
         new HashMap<String, ConditionChecker>();
+    checkers.put(timeChecker.getId(), timeChecker);
+    String expr = timeChecker.getId() + ".eval()";
+
+    return new Condition(checkers, expr);
+  }
+
+  @Test
+  public void periodTimerTest() {
 
     // get a new timechecker, start from now, repeat every minute. should
     // evaluate to false now, and true a minute later.
@@ -45,12 +53,9 @@ public class BasicTimeCheckerTest {
 
     BasicTimeChecker timeChecker =
         new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
-            now.getZone(), true, true, period);
-    checkers.put(timeChecker.getId(), timeChecker);
-    String expr = timeChecker.getId() + ".eval()";
+            now.getZone(), true, true, period, null);
 
-    Condition cond = new Condition(checkers, expr);
-    System.out.println(expr);
+    Condition cond = getCondition(timeChecker);
 
     assertFalse(cond.isMet());
 
@@ -63,9 +68,7 @@ public class BasicTimeCheckerTest {
     }
 
     assertTrue(cond.isMet());
-
     cond.resetCheckers();
-
     assertFalse(cond.isMet());
 
     // sleep for 1 min
@@ -77,6 +80,191 @@ public class BasicTimeCheckerTest {
     }
 
     assertTrue(cond.isMet());
+  }
+
+  /**
+   * Test Base Cron Functionality.
+   */
+  @Test
+  public void testQuartzCurrentZone() {
+
+    DateTime now = DateTime.now();
+    String cronExpression = "0 0 0 31 12 ? 2050";
+
+    BasicTimeChecker timeChecker =
+        new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+            now.getZone(), true, true, null, cronExpression);
+    System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+    Condition cond = getCondition(timeChecker);
+    // 2556086400000L represent for "2050-12-31T00:00:00.000-08:00"
+
+    DateTime year2050 = new DateTime(2050, 12, 31, 0 ,0 ,0 ,now.getZone());
+    assertTrue(cond.getNextCheckTime() == year2050.getMillis());
+  }
+
+  /**
+   * Test when PST-->PDT happens in 2020. -8:00 -> -7:00
+   * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+   *
+   * This test demonstrates that if the cron is under UTC settings,
+   * When daylight saving change occurs, 2:30 will be changed to 3:30 at that day.
+   */
+  @Test
+  public void testPSTtoPDTunderUTC() {
+
+    DateTime now = DateTime.now();
+
+    // 10:30 UTC == 2:30 PST
+    String cronExpression = "0 30 10 8 3 ? 2020";
+
+    BasicTimeChecker timeChecker =
+        new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+            DateTimeZone.UTC, true, true, null, cronExpression);
+    System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+    Condition cond = getCondition(timeChecker);
+
+    DateTime spring2020UTC = new DateTime(2020, 3, 8, 10, 30, 0, DateTimeZone.UTC);
+    DateTime spring2020PDT = new DateTime(2020, 3, 8, 3, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+    assertTrue(cond.getNextCheckTime() == spring2020UTC.getMillis());
+    assertTrue(cond.getNextCheckTime() == spring2020PDT.getMillis());
+  }
+
+  /**
+   * Test when PST-->PDT happens in 2020. -8:00 -> -7:00
+   * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+   *
+   * This test demonstrates that 2:30 AM will not happen during the daylight saving day on Cron settings under PDT/PST.
+   * Since we let the cron triggered both at March 8th, and 9th, it will execute at March 9th.
+   */
+  @Test
+  public void testPSTtoPDTdst2() {
+
+    DateTime now = DateTime.now();
+
+    String cronExpression = "0 30 2 8,9 3 ? 2020";
+
+    BasicTimeChecker timeChecker =
+        new BasicTimeChecker("BasicTimeChecker_1", now.getMillis(),
+            DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+    System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+    Condition cond = getCondition(timeChecker);
+
+    DateTime aTime = new DateTime(2020, 3, 9, 2, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+    assertTrue(cond.getNextCheckTime() == aTime.getMillis());
+  }
+
+  /**
+   * Test when PDT-->PST happens in 2020. -7:00 -> -8:00
+   * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+   *
+   * This test cronDayLightPacificWinter1 is in order to compare against the cronDayLightPacificWinter2.
+   *
+   * In this Test, we let job run at 1:00 at Nov.1st, 2020. We know that we will have two 1:00 at that day.
+   * The test shows that the first 1:00 is skipped at that day.
+   * Schedule will still be executed once on that day.
+   */
+  @Test
+  public void testPDTtoPSTdst1() {
+
+    DateTime now = DateTime.now();
+
+    // 9:00 UTC == 1:00 PST (difference is 8 hours)
+    String cronExpression = "0 0 1 1,2 11 ? 2020";
+
+    BasicTimeChecker timeChecker =
+        new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+            DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+    System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+    Condition cond = getCondition(timeChecker);
+
+    DateTime winter2020 = new DateTime(2020, 11, 1, 9, 0, 0, DateTimeZone.UTC);
+
+    DateTime winter2020_2 = new DateTime(2020, 11, 1, 1, 0, 0, DateTimeZone.forID("America/Los_Angeles"));
+    DateTime winter2020_3 = new DateTime(2020, 11, 1, 2, 0, 0, DateTimeZone.forID("America/Los_Angeles"));
+    assertTrue(cond.getNextCheckTime() == winter2020.getMillis());
+
+
+    // Both 1 and 2 o'clock can not pass the test. Based on milliseconds we got,
+    // winter2020_2.getMillis() == 11/1/2020, 1:00:00 AM GMT-7:00 DST
+    // winter2020_3.getMillis() == 11/1/2020, 2:00:00 AM GMT-8:00
+    // Both time doesn't match the second 1:00 AM
+    assertFalse(cond.getNextCheckTime() == winter2020_2.getMillis());
+    assertFalse(cond.getNextCheckTime() == winter2020_3.getMillis());
+  }
+
+
+  /**
+   * Test when PDT-->PST happens in 2020. -7:00 -> -8:00
+   * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+   *
+   * This test cronDayLightPacificWinter2 is in order to be compared against the cronDayLightPacificWinter1.
+   *
+   * In this Test, we let job run at 0:59 at Nov.1st, 2020. it shows that it is 7:59 UTC
+   * The test shows 7:59 UTC jump to 9:00 UTC.
+   */
+  @Test
+  public void testPDTtoPSTdst2() {
+
+    DateTime now = DateTime.now();
+
+    // 7:59 UTC == 0:59 PDT (difference is 7 hours)
+    String cronExpression = "0 59 0 1,2 11 ? 2020";
+
+    BasicTimeChecker timeChecker =
+        new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+            DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+    System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+    Condition cond = getCondition(timeChecker);
+
+    // 7:59 UTC == 0:59 PDT (difference is 7 hours)
+    DateTime winter2020 = new DateTime(2020, 11, 1, 7, 59, 0, DateTimeZone.UTC);
+    DateTime winter2020_2 = new DateTime(2020, 11, 1, 0, 59, 0, DateTimeZone.forID("America/Los_Angeles"));
+
+    // Local time remains the same.
+    assertTrue(cond.getNextCheckTime() == winter2020.getMillis());
+    assertTrue(cond.getNextCheckTime() == winter2020_2.getMillis());
+  }
+
+
+
+  /**
+   * Test when PDT-->PST happens in 2020. -7:00 -> -8:00
+   * See details why confusion happens during this change: https://en.wikipedia.org/wiki/Pacific_Time_Zone
+   *
+   * This test is a supplement to cronDayLightPacificWinter1.
+   *
+   * Still, we let job run at 1:30 at Nov.1st, 2020. We know that we will have two 1:30 at that day.
+   * The test shows the 1:30 at that day will be based on PST, not PDT. It means that the first 1:30 is skipped at that day.
+   */
+  @Test
+  public void testPDTtoPSTdst3() {
+    
+    DateTime now = DateTime.now();
+
+    // 9:30 UTC == 1:30 PST (difference is 8 hours)
+    String cronExpression = "0 30 1 1,2 11 ? 2020";
+
+    BasicTimeChecker timeChecker =
+        new BasicTimeChecker("BasicTimeChecket_1", now.getMillis(),
+            DateTimeZone.forID("America/Los_Angeles"), true, true, null, cronExpression);
+    System.out.println("getNextCheckTime = " + timeChecker.getNextCheckTime());
+
+    Condition cond = getCondition(timeChecker);
+
+    // 9:30 UTC == 1:30 PST (difference is 8 hours)
+    DateTime winter2020 = new DateTime(2020, 11, 1, 9, 30, 0, DateTimeZone.UTC);
+
+    DateTime winter2020_2 = new DateTime(2020, 11, 1, 1, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+    DateTime winter2020_3 = new DateTime(2020, 11, 1, 2, 30, 0, DateTimeZone.forID("America/Los_Angeles"));
+    assertTrue(cond.getNextCheckTime() == winter2020.getMillis());
 
+    // Both 1:30 and 2:30 can not pass the test.
+    assertFalse(cond.getNextCheckTime() == winter2020_2.getMillis());
+    assertFalse(cond.getNextCheckTime() == winter2020_3.getMillis());
   }
 }
diff --git a/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java b/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
index cfff784..cf5a1fa 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/ConditionTest.java
@@ -85,7 +85,7 @@ public class ConditionTest {
     // period);
     ConditionChecker timeChecker =
         new BasicTimeChecker("BasicTimeChecker_1", now.getMillis(),
-            now.getZone(), true, true, Utils.parsePeriodString(period));
+            now.getZone(), true, true, Utils.parsePeriodString(period), null);
     System.out.println("checker id is " + timeChecker.getId());
 
     checkers.put(timeChecker.getId(), timeChecker);
diff --git a/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java b/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
index 01fbb29..b55a2ba 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/JdbcTriggerLoaderTest.java
@@ -206,7 +206,7 @@ public class JdbcTriggerLoaderTest {
     DateTime now = DateTime.now();
     ConditionChecker checker1 =
         new BasicTimeChecker("timeChecker1", now.getMillis(), now.getZone(),
-            true, true, Utils.parsePeriodString("1h"));
+            true, true, Utils.parsePeriodString("1h"), null);
     Map<String, ConditionChecker> checkers1 =
         new HashMap<String, ConditionChecker>();
     checkers1.put(checker1.getId(), checker1);
diff --git a/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java b/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
index a0e1f51..595461f 100644
--- a/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
+++ b/azkaban-common/src/test/java/azkaban/trigger/TriggerTest.java
@@ -57,7 +57,7 @@ public class TriggerTest {
     DateTime now = DateTime.now();
     ConditionChecker checker1 =
         new BasicTimeChecker("timeChecker1", now.getMillis(), now.getZone(),
-            true, true, Utils.parsePeriodString("1h"));
+            true, true, Utils.parsePeriodString("1h"), null);
     Map<String, ConditionChecker> checkers1 =
         new HashMap<String, ConditionChecker>();
     checkers1.put(checker1.getId(), checker1);
diff --git a/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java b/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
index 85260d5..013b410 100644
--- a/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
+++ b/azkaban-execserver/src/test/java/azkaban/execapp/event/BlockingStatusTest.java
@@ -44,9 +44,9 @@ public class BlockingStatusTest {
   }
 
   /**
-   * TODO: Ignore this test at present since travis in Github can not always pass this test.
-   *       We will modify the below code to make travis pass in future.
-   */
+    * TODO: Ignore this test at present since travis in Github can not always pass this test.
+    *       We will modify the below code to make travis pass in future.
+    */
   @Ignore @Test
   public void testFinishedBlock() {
     BlockingStatus status = new BlockingStatus(1, "test", Status.SKIPPED);
diff --git a/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java b/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
index b2a7e1e..079fe04 100644
--- a/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
+++ b/azkaban-migration/src/main/java/azkaban/migration/schedule2trigger/Schedule2Trigger.java
@@ -210,6 +210,7 @@ public class Schedule2Trigger {
         DateTimeZone timezone = DateTimeZone.forID(timezoneId);
         ReadablePeriod period =
             Utils.parsePeriodString(schedProps.getString("period"));
+        String cronExpression = schedProps.getString("cronExpression");
         // DateTime lastModifyTime = DateTime.now();
         long nextExecTimeLong = schedProps.getLong("nextExecTimeLong");
         // DateTime nextExecTime = new DateTime(nextExecTimeLong);
@@ -246,7 +247,7 @@ public class Schedule2Trigger {
             new azkaban.scheduler.Schedule(-1, projectId, projectName,
                 flowName, "ready", firstSchedTimeLong, timezone, period,
                 DateTime.now().getMillis(), nextExecTimeLong, submitTimeLong,
-                submitUser, executionOptions, slaOptions);
+                submitUser, executionOptions, slaOptions, cronExpression);
         Trigger t = scheduleToTrigger(schedule);
         logger.info("Ready to insert trigger " + t.getDescription());
         triggerLoader.addTrigger(t);
@@ -290,7 +291,7 @@ public class Schedule2Trigger {
     ConditionChecker checker =
         new BasicTimeChecker("BasicTimeChecker_1", s.getFirstSchedTime(),
             s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
-            s.getPeriod());
+            s.getPeriod(), s.getCronExpression());
     checkers.put(checker.getId(), checker);
     String expr = checker.getId() + ".eval()";
     Condition cond = new Condition(checkers, expr);
@@ -305,7 +306,7 @@ public class Schedule2Trigger {
     ConditionChecker checker =
         new BasicTimeChecker("BasicTimeChecker_2", s.getFirstSchedTime(),
             s.getTimezone(), s.isRecurring(), s.skipPastOccurrences(),
-            s.getPeriod());
+            s.getPeriod(),s.getCronExpression());
     checkers.put(checker.getId(), checker);
     String expr = checker.getId() + ".eval()";
     Condition cond = new Condition(checkers, expr);
diff --git a/azkaban-webserver/build.gradle b/azkaban-webserver/build.gradle
index 7440c4a..2801f9b 100644
--- a/azkaban-webserver/build.gradle
+++ b/azkaban-webserver/build.gradle
@@ -17,6 +17,10 @@ bower {
       source 'build/js/*.min.js'  >> '/'
       excludes 'jquery'
     }
+
+    'later'('1.2.0'){
+      source 'later.min.js' >> '/'
+    }
 }
 
 apply plugin: 'lesscss'
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index 12c5993..b19021d 100644
--- a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TimeZone;
 
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
@@ -30,8 +31,6 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.joda.time.DateTime;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
 
 import azkaban.server.AzkabanServer;
 import azkaban.server.HttpRequestUtils;
@@ -48,8 +47,6 @@ import azkaban.webapp.plugin.TriggerPlugin;
  * Base Servlet for pages
  */
 public abstract class AbstractAzkabanServlet extends HttpServlet {
-  private static final DateTimeFormatter ZONE_FORMATTER = DateTimeFormat
-      .forPattern("z");
   private static final String AZKABAN_SUCCESS_MESSAGE =
       "azkaban.success.message";
   private static final String AZKABAN_WARN_MESSAGE =
@@ -72,6 +69,12 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
   private String label;
   private String color;
 
+  /*
+   * The variable schedulePanelPageName is in charge of switching on retired schedulePanelDeprecated.vm (old UI)
+   * or the new schedulePanel.vm (new UI). We can configure it in conf for this binary change.
+   */
+  private String schedulePanelPageName;
+
   private List<ViewerPlugin> viewerPlugins;
   private List<TriggerPlugin> triggerPlugins;
 
@@ -99,6 +102,7 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
     name = props.getString("azkaban.name", "");
     label = props.getString("azkaban.label", "");
     color = props.getString("azkaban.color", "#FF0000");
+    schedulePanelPageName = props.getString("azkaban.schedulePanelPageName", "schedulepanelDeprecated.vm");
 
     if (application instanceof AzkabanWebServer) {
       AzkabanWebServer server = (AzkabanWebServer) application;
@@ -330,8 +334,9 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
     page.add("azkaban_name", name);
     page.add("azkaban_label", label);
     page.add("azkaban_color", color);
+    page.add("switchToPanelPage", schedulePanelPageName);
     page.add("utils", utils);
-    page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
+    page.add("timezone", TimeZone.getDefault().getID());
     page.add("currentTime", (new DateTime()).getMillis());
     if (session != null && session.getUser() != null) {
       page.add("user_id", session.getUser().getUserId());
@@ -380,7 +385,8 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
     page.add("azkaban_name", name);
     page.add("azkaban_label", label);
     page.add("azkaban_color", color);
-    page.add("timezone", ZONE_FORMATTER.print(System.currentTimeMillis()));
+    page.add("switchToPanelPage", schedulePanelPageName);
+    page.add("timezone", TimeZone.getDefault().getID());
     page.add("currentTime", (new DateTime()).getMillis());
     page.add("context", req.getContextPath());
 
diff --git a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java
index a36505b..14d5b89 100644
--- a/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java
+++ b/azkaban-webserver/src/main/java/azkaban/webapp/servlet/ScheduleServlet.java
@@ -114,6 +114,8 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
       ret = null;
     } else if (ajaxName.equals("scheduleFlow")) {
       ajaxScheduleFlow(req, ret, session.getUser());
+    } else if (ajaxName.equals("scheduleCronFlow")) {
+      ajaxScheduleCronFlow(req, ret, session.getUser());
     } else if (ajaxName.equals("fetchSchedule")) {
       ajaxFetchSchedule(req, ret, session.getUser());
     }
@@ -249,6 +251,7 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
         jsonObj.put("nextExecTime",
             utils.formatDateTime(schedule.getNextExecTime()));
         jsonObj.put("period", utils.formatPeriod(schedule.getPeriod()));
+        jsonObj.put("cronExpression", schedule.getCronExpression());
         jsonObj.put("executionOptions", schedule.getExecutionOptions());
         ret.put("schedule", jsonObj);
       }
@@ -384,6 +387,8 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
         String action = getParam(req, "action");
         if (action.equals("scheduleFlow")) {
           ajaxScheduleFlow(req, ret, session.getUser());
+        } else if (action.equals("scheduleCronFlow")) {
+          ajaxScheduleCronFlow(req, ret, session.getUser());
         } else if (action.equals("removeSched")) {
           ajaxRemoveSched(req, ret, session.getUser());
         }
@@ -684,6 +689,87 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
     ret.put("message", projectName + "." + flowName + " scheduled.");
   }
 
+  /**
+   *
+   * This method is in charge of doing cron scheduling.
+   * @throws ServletException
+   */
+  private void ajaxScheduleCronFlow(HttpServletRequest req,
+      HashMap<String, Object> ret, User user) throws ServletException {
+    String projectName = getParam(req, "projectName");
+    String flowName = getParam(req, "flow");
+
+    Project project = projectManager.getProject(projectName);
+
+    if (project == null) {
+      ret.put("message", "Project " + projectName + " does not exist");
+      ret.put("status", "error");
+      return;
+    }
+    int projectId = project.getId();
+
+    if (!hasPermission(project, user, Type.SCHEDULE)) {
+      ret.put("status", "error");
+      ret.put("message", "Permission denied. Cannot execute " + flowName);
+      return;
+    }
+
+    Flow flow = project.getFlow(flowName);
+    if (flow == null) {
+      ret.put("status", "error");
+      ret.put("message", "Flow " + flowName + " cannot be found in project "
+          + project);
+      return;
+    }
+
+    DateTimeZone timezone = DateTimeZone.getDefault();
+    DateTime firstSchedTime = getPresentTimeByTimezone(timezone);
+
+    String cronExpression = null;
+    try {
+      if (hasParam(req, "cronExpression")) {
+        // everything in Azkaban functions is at the minute granularity, so we add 0 here
+        // to let the expression to be complete.
+        cronExpression = getParam(req, "cronExpression");
+        if(azkaban.utils.Utils.isCronExpressionValid(cronExpression) == false) {
+          ret.put("error", "This expression <" + cronExpression + "> can not be parsed to quartz cron.");
+          return;
+        }
+      }
+      if(cronExpression == null)
+        throw new Exception("Cron expression must exist.");
+    } catch (Exception e) {
+      ret.put("error", e.getMessage());
+    }
+
+    ExecutionOptions flowOptions = null;
+    try {
+      flowOptions = HttpRequestUtils.parseFlowOptions(req);
+      HttpRequestUtils.filterAdminOnlyFlowParams(userManager, flowOptions, user);
+    } catch (Exception e) {
+      ret.put("error", e.getMessage());
+    }
+
+    List<SlaOption> slaOptions = null;
+
+    // Because either cronExpression or recurrence exists, we build schedule in the below way.
+    Schedule schedule = scheduleManager.cronScheduleFlow(-1, projectId, projectName, flowName,
+            "ready", firstSchedTime.getMillis(), firstSchedTime.getZone(),
+            DateTime.now().getMillis(), firstSchedTime.getMillis(),
+            firstSchedTime.getMillis(), user.getUserId(), flowOptions,
+            slaOptions, cronExpression);
+
+    logger.info("User '" + user.getUserId() + "' has scheduled " + "["
+        + projectName + flowName + " (" + projectId + ")" + "].");
+    projectManager.postProjectEvent(project, EventType.SCHEDULE,
+        user.getUserId(), "Schedule " + schedule.toString()
+            + " has been added.");
+
+    ret.put("status", "success");
+    ret.put("scheduleId", schedule.getScheduleId());
+    ret.put("message", projectName + "." + flowName + " scheduled.");
+  }
+
   private DateTime parseDateTime(String scheduleDate, String scheduleTime) {
     // scheduleTime: 12,00,pm,PDT
     String[] parts = scheduleTime.split(",", -1);
@@ -713,4 +799,19 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 
     return firstSchedTime;
   }
+
+  /**
+   * @param cronTimezone represents the timezone from remote API call
+   * @return if the string is equal to UTC, we return UTC; otherwise, we always return default timezone.
+   */
+  private DateTimeZone parseTimeZone(String cronTimezone) {
+    if(cronTimezone != null && cronTimezone.equals("UTC"))
+      return DateTimeZone.UTC;
+
+    return DateTimeZone.getDefault();
+  }
+
+  private DateTime getPresentTimeByTimezone(DateTimeZone timezone) {
+    return new DateTime(timezone);
+  }
 }
diff --git a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
index f526573..99c7541 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
@@ -201,7 +201,7 @@
       </div><!-- /modal -->
 
 #if (!$show_schedule || $show_schedule == 'true')
-  #parse ("azkaban/webapp/servlet/velocity/schedulepanel.vm")
+  #parse ("azkaban/webapp/servlet/velocity/${switchToPanelPage}")
 #end
 
 #*
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
index 6657fb1..663738d 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/scheduledflowpage.vm
@@ -36,7 +36,7 @@
       var timezone = "${timezone}";
       var errorMessage = null;
       var successMessage = null;
-      
+
       $(document).ready(function () {
         var flowTable = $("#scheduledFlowsTbl");
         flowTable.tablesorter();
@@ -80,6 +80,7 @@
                 <th class="date">First Scheduled to Run</th>
                 <th class="date">Next Execution Time</th>
                 <th class="date">Repeats Every</th>
+                <th class="date">Cron Expression</th>
                 <th>Has SLA</th>
                 <th colspan="2" class="action ignoresort">Action</th>
               </tr>
@@ -101,7 +102,16 @@
                 <td>${sched.submitUser}</td>
                 <td>$utils.formatDateTime(${sched.firstSchedTime})</td>
                 <td>$utils.formatDateTime(${sched.nextExecTime})</td>
-                <td>$utils.formatPeriod(${sched.period})</td>
+                #if (${sched.period})
+                  <td> $utils.formatPeriod(${sched.period}) </td>
+                #else
+                  <td>Not Applicable</td>
+                #end
+                #if (${sched.cronExpression})
+                  <td> ${sched.cronExpression} </td>
+                #else
+                  <td>Not Applicable</td>
+                #end
                 <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>
@@ -109,7 +119,7 @@
     #end
   #else
               <tr>
-                <td colspan="10">No scheduled flow found.</td>
+                <td colspan="11">No scheduled flow found.</td>
               </tr>
   #end
             </tbody>
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
index 8aff70f..3f373eb 100644
--- a/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanel.vm
@@ -14,63 +14,116 @@
  * 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>
+<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>
+<script type="text/javascript" src="${context}/js/moment.min.js" ></script>
+<script type="text/javascript" src="${context}/js/later.min.js"></script>
+<script type="text/javascript" src="${context}/js/moment-timezone-with-data-2010-2020.min.js"></script>
+<style type="text/css">
+  .input-box { position: relative; }
+  .input-box input { display: block; border: 1px solid #d7d6d6; background: #fff; padding: 10px 10px 10px 20px; width: 195px; }
+  .unit { position: absolute; display: block; left: 5px; top: 7px; z-index: 9; }
+</style>
+<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">&times;</button>
+        <h4 class="modal-title">Schedule Flow Options</h4>
+      </div><!-- /modal-header -->
+      <div class="modal-body">
+        <fieldset class="form-horizontal">
+          <div class="form-group">
 
-      <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">&times;</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 class="col-sm-12" style="height:55px;">
+              <h4 style="color:Coral; font-style:italic;">All schedules are basead on the server timezone: <b id="timeZoneID"></b>.</h4>
+
+            </div>
+            <div class="form-group">
+              <div class="col-sm-6">
+                <label class="col-sm-3 control-label" id="min_label">Min</label>
+                <div class="col-sm-9">
+                  <input type="text" id="minute_input" value="0" class="form-control" oninput="updateOutput()">
+                </div>
+                <br/> <br/> <br/>
+                <label class="col-sm-3 control-label" id="hour_label">Hours</label>
+                <div class="col-sm-9">
+                  <input type="text" id="hour_input" value="5,7-10" class="form-control"
+                         oninput="updateOutput()">
                 </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>
+                <br/> <br/> <br/>
+                <label class="col-sm-3 control-label" id="dom_label">DOM</label>
+                <div class="col-sm-9">
+                  <input type="text" id="dom_input" value="?" class="form-control"
+                         oninput="updateOutput()">
                 </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>
+                <br/> <br/> <br/>
+                <label class="col-sm-3 control-label" id="mon_label">Month</label>
+                <div class="col-sm-9">
+                  <input type="text" id="month_input" value="*" class="form-control"
+                         oninput="updateOutput()">
                 </div>
-              </fieldset>
+                <br/> <br/> <br/>
+                <label class="col-sm-3 control-label" id="dow_label">DOW</label>
+                <div class="col-sm-9">
+                  <input type="text" id="dow_input" value="4-6" class="form-control"
+                         oninput="updateOutput()">
+                </div>
+              </div>
+
+              <div class="col-sm-5" style="background-color:#f5f5f5; border:1px solid #e3e3e3">
+                <h4 style="color:orange">Special Characters:</h4>
+                <table class="table table-striped" data-row-style="rowColors" id="instructions">
+                  <tbody>
+                  <tr class="success">
+                    <th scope="row">*</th>
+                    <td>any value</td>
+                  </tr>
+                  <tr class="primary">
+                    <th scope="row">,</th>
+                    <td>value list separators</td>
+                  </tr>
+                  <tr class="warning">
+                    <th scope="row">-</th>
+                    <td>range of values</td>
+                  </tr>
+                  <tr class="danger">
+                    <th scope="row">/</th>
+                    <td>step values</td>
+                  </tr>
+                  </tbody>
+                </table>
+                <p><u><span style="color:Indigo"><a href="http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html">Detailed instructions</a></span>.</u></p>
+              </div>
             </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 class="col-sm-offset-1 col-sm-4">
+              <div class="input-box">
+                <input type="text" id="cron-output" value="0 5,7-10 ? * 4-6" oninput = "updateExpression()" class="form-control">
+                <span class="unit">0</span>
+              </div>
             </div>
-          </div>
-        </div>
+            <button type="button" class="col-sm-offset-1 btn btn-warning" id="clearCron">Reset</button>
+            <div class="col-xs-12" style="height:20px;"></div>
+
+            <div class="col-sm-12" style="background-color:#f5f5f5; border:1px solid #e3e3e3">
+              <h4>
+                Next 10 scheduled executions:
+              </h4>
+              <ul id="nextRecurId">
+              </ul>
+            </div>
+
+            <h3 id="cronTranslate" style="color:DeepSkyBlue; font-style:italic;"></h3>
+            <div class="col-sm-9 col-sm-offset-3">
+              <p class="text-right" id="translationWarning"> </p>
+            </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/schedulepanelDeprecated.vm b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanelDeprecated.vm
new file mode 100644
index 0000000..48df207
--- /dev/null
+++ b/azkaban-webserver/src/main/resources/azkaban/webapp/servlet/velocity/schedulepanelDeprecated.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-panelDeprecated.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">&times;</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>
\ No newline at end of file
diff --git a/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js b/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
index 249f1e4..803622d 100644
--- a/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
+++ b/azkaban-webserver/src/web/js/azkaban/view/schedule-panel.js
@@ -23,8 +23,6 @@ azkaban.SchedulePanelView = Backbone.View.extend({
   },
 
   initialize: function(settings) {
-    $("#timepicker").datetimepicker({format: 'LT'});
-    $("#datepicker").datetimepicker({format: 'L'});
   },
 
   render: function() {
@@ -39,40 +37,30 @@ azkaban.SchedulePanelView = Backbone.View.extend({
   },
 
   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);
+    console.log("Creating schedule for " + projectName + "." + scheduleData.flow);
 
-    var scheduleTime = moment(timeVal, 'h/mm A').format('h,mm,A,') + timezoneVal;
-    console.log(scheduleTime);
+    var currentMomentTime = moment();
+    var scheduleTime = currentMomentTime.utc().format('h,mm,A,')+"UTC";
+    var scheduleDate = currentMomentTime.format('MM/DD/YYYY');
 
-    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.ajax = "scheduleCronFlow";
     scheduleData.projectName = projectName;
-    scheduleData.scheduleTime = scheduleTime;
-    scheduleData.scheduleDate = scheduleDate;
-    scheduleData.is_recurring = is_recurring;
-    scheduleData.period = period;
+    scheduleData.cronExpression = "0 " + $('#cron-output').val();
+
+    // Currently, All cron expression will be based on server timezone.
+    // Later we might implement a feature support cron under various timezones, depending on the future use cases.
+    // scheduleData.cronTimezone = timezone;
+
+    console.log("current Time = " + scheduleDate + "  " + scheduleTime );
+    console.log("cronExpression = " +  scheduleData.cronExpression);
 
     var successHandler = function(data) {
       if (data.error) {
         schedulePanelView.hideSchedulePanel();
-        messageDialogView.show("Error Scheduling Flow", data.message);
+        messageDialogView.show("Error Scheduling Flow", data.error);
       }
       else {
         schedulePanelView.hideSchedulePanel();
@@ -90,4 +78,163 @@ $(function() {
   schedulePanelView =  new azkaban.SchedulePanelView({
     el: $('#schedule-modal')
   });
+
+  // To compute the current timezone's time offset against UTC.
+  // Currently not useful.
+  // var TimeZoneOffset = new Date().toString().match(/([-\+][0-9]+)\s/)[1];
+
+  $('#timeZoneID').html(timezone);
+
+  updateOutput();
+  $("#clearCron").click(function () {
+    $('#cron-output').val("* * * * ?");
+    resetLabelColor();
+    $("#minute_input").val("*");
+    $("#hour_input").val("*");
+    $("#dom_input").val("*");
+    $("#month_input").val("*");
+    $("#dow_input").val("?");
+    $(cron_translate_id).text("")
+    $(cron_translate_warning_id).text("")
+    $('#nextRecurId').html("");
+
+    while ($("#instructions tbody tr:last").index() >= 4) {
+      $("#instructions tbody tr:last").remove();
+    }
+  });
+
+  $("#minute_input").click(function () {
+    while ($("#instructions tbody tr:last").index() >= 4) {
+      $("#instructions tbody tr:last").remove();
+    }
+    resetLabelColor();
+    $("#min_label").css("color", "red");
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last th').html("0-59");
+    $('#instructions tbody tr:last td').html("allowed values");
+  });
+
+  $("#hour_input").click(function () {
+    while ($("#instructions tbody tr:last").index() >= 4) {
+      $("#instructions tbody tr:last").remove();
+    }
+    resetLabelColor();
+    $("#hour_label").css("color", "red");
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last th').html("0-23");
+    $('#instructions tbody tr:last td').html("allowed values");
+  });
+
+  $("#dom_input").click(function () {
+    while ($("#instructions tbody tr:last").index() >= 4) {
+      $("#instructions tbody tr:last").remove();
+    }
+    resetLabelColor();
+    $("#dom_label").css("color", "red");
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last th').html("1-31");
+    $('#instructions tbody tr:last td').html("allowed values");
+
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last').find('td').css({'class': 'danger'});
+    $('#instructions tbody tr:last th').html("?");
+    $('#instructions tbody tr:last td').html("Blank");
+  });
+
+  $("#month_input").click(function () {
+    while ($("#instructions tbody tr:last").index() >= 4) {
+      $("#instructions tbody tr:last").remove();
+    }
+    resetLabelColor();
+    $("#mon_label").css("color", "red");
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last th').html("1-12");
+    $('#instructions tbody tr:last td').html("allowed values");
+  });
+
+  $("#dow_input").click(function () {
+    while ($("#instructions tbody tr:last").index() >= 4) {
+      $("#instructions tbody tr:last").remove();
+    }
+    resetLabelColor();
+    $("#dow_label").css("color", "red");
+
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last th').html("1-7");
+    $('#instructions tbody tr:last td').html("SUN MON TUE WED THU FRI SAT");
+
+    $('#instructions tbody').append($("#instructions tbody tr:first").clone());
+    $('#instructions tbody tr:last th').html("?");
+    $('#instructions tbody tr:last td').html("Blank");
+  });
 });
+
+function resetLabelColor(){
+  $("#min_label").css("color", "black");
+  $("#hour_label").css("color", "black");
+  $("#dom_label").css("color", "black");
+  $("#mon_label").css("color", "black");
+  $("#dow_label").css("color", "black");
+}
+
+var cron_minutes_id = "#minute_input";
+var cron_hours_id   = "#hour_input";
+var cron_dom_id     = "#dom_input";
+var cron_months_id  = "#month_input";
+var cron_dow_id     = "#dow_input";
+var cron_output_id  = "#cron-output";
+var cron_translate_id  = "#cronTranslate";
+var cron_translate_warning_id  = "#translationWarning";
+
+// Cron use 0-6 as Sun--Sat, but Quartz use 1-7. Therefore, a translation is necessary.
+function transformFromCronToQuartz(str){
+  var res = str.split(" ");
+  res[res.length -1] = res[res.length -1].replace(/[0-7]/g, function upperToHyphenLower(match) {
+    return (parseInt(match)+6)%7;
+  });
+  return res.join(" ");
+}
+
+function updateOutput() {
+  $(cron_output_id).val( $(cron_minutes_id).val() + " " +  $(cron_hours_id).val() + " " +
+      $(cron_dom_id).val() + " " + $(cron_months_id).val() + " " + $(cron_dow_id).val()
+  );
+  updateExpression();
+}
+
+function updateExpression() {
+  $('#nextRecurId').html("");
+
+  console.log("cron Input = " + $(cron_output_id).val());
+  var laterCron = later.parse.cron($(cron_output_id).val());
+
+  //Get the current time given the server timezone.
+  var serverTime = moment().tz(timezone);
+  console.log("serverTime = " + serverTime.format());
+  var now1Str = serverTime.format();
+
+  //Get the server Timezone offset against UTC (e.g. if timezone is PDT, it should be -07:00)
+  // var timeZoneOffset = now1Str.substring(now1Str.length-6, now1Str.length);
+  // console.log("offset = " + timeZoneOffset);
+
+  //Transform the moment time to UTC Date time (required by later.js)
+  var serverTimeInJsDateFormat = new Date();
+  serverTimeInJsDateFormat.setUTCHours(serverTime.get('hour'), serverTime.get('minute'), 0, 0);
+  serverTimeInJsDateFormat.setUTCMonth(serverTime.get('month'), serverTime.get('date'));
+
+  // Calculate the following 10 occurences based on the current server time.
+  // The logic is a bit tricky here. since later.js only support UTC Date (javascript raw library).
+  // We transform from current browser-timezone-time to Server timezone.
+  // Then we let serverTimeInJsDateFormat is equal to the server time.
+  var occurrences = later.schedule(laterCron).next(10, serverTimeInJsDateFormat);
+
+  //The following component below displays a list of next 10 triggering timestamp.
+  for(var i = 9; i >= 0; i--) {
+    var strTime = JSON.stringify(occurrences[i]);
+
+    // Get the time. The original occurance time string is like: "2016-09-09T05:00:00.999",
+    // We trim the string to ignore milliseconds.
+    var nextTime = '<li style="color:DarkGreen">' + strTime.substring(1, strTime.length-6) + '</li>';
+    $('#nextRecurId').prepend(nextTime);
+  }
+}
diff --git a/azkaban-webserver/src/web/js/azkaban/view/schedule-panelDeprecated.js b/azkaban-webserver/src/web/js/azkaban/view/schedule-panelDeprecated.js
new file mode 100644
index 0000000..249f1e4
--- /dev/null
+++ b/azkaban-webserver/src/web/js/azkaban/view/schedule-panelDeprecated.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({format: 'LT'});
+    $("#datepicker").datetimepicker({format: 'L'});
+  },
+
+  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')
+  });
+});