azkaban-aplcache

Fix pagination bugs on Flow page: (#2120) -Next and Previous

2/4/2019 5:47:39 PM

Details

diff --git a/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/flowpage.vm b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/flowpage.vm
index bcbb9ab..abfbaa1 100644
--- a/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -25,6 +25,7 @@
   <script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
   <script type="text/javascript" src="${context}/js/raphael.min.js"></script>
   <script type="text/javascript" src="${context}/js/morris.min.js"></script>
+  <script type="text/javascript" src="${context}/js/jquery.twbsPagination.min.js"></script>
 
   <script type="text/javascript" src="${context}/js/dust-full-2.2.3.min.js"></script>
   <script type="text/javascript" src="${context}/js/flowsummary.js?version=20161003"></script>
@@ -49,6 +50,28 @@
     var flowId = "${flowid}";
     var execId = null;
     var pageSize = "${size}";
+
+    var flowPageSettings = {
+      projectId: ${project.id},
+      projectName: "${project.name}",
+      flowId: "${flowid}",
+      pageSize: ${size},
+
+      executionUrl: contextURL + "/executor",
+      triggerInstanceUrl: contextURL + "/executor",
+      fetchFlowExecutionsUrl: contextURL + "/manager",
+      fetchTriggerInstancesUrl: contextURL + "/flowtriggerinstance",
+      fetchFlowDetailsUrl: contextURL + "/manager",
+      fetchScheduleUrl: contextURL + "/schedule",
+      slaInfoUrl: contextURL + "/schedule",
+      fetchLastSuccessfulFlowExecutionUrl: contextURL + "/manager",
+      fetchTriggerUrl: contextURL + "/flowtrigger",
+      fetchFlowGraphUrl: contextURL + "/manager",
+    };
+
+    $(function () {
+      initFlowPage(flowPageSettings);
+    });
   </script>
   <link rel="stylesheet" type="text/css" href="${context}/css/morris.css"/>
   <link rel="stylesheet" type="text/css" href="${context}/css/bootstrap-datetimepicker.css"/>
@@ -74,8 +97,8 @@
         </div>
         <div class="header-control">
           <div class="pull-right header-form">
-            <button type="button" class="btn btn-sm btn-success" id="executebtn">Schedule / Execute
-              Flow
+            <button type="button" class="btn btn-sm btn-success" id="executebtn">
+              Schedule / Execute Flow
             </button>
           </div>
         </div>
@@ -135,15 +158,8 @@
           <tbody id="execTableBody">
           </tbody>
         </table>
-        <ul id="pageSelection" class="pagination">
-          <li id="previous" class="first"><a><span class="arrow">&larr;</span>Previous</a></li>
-          <li id="page1"><a href="#page1">1</a></li>
-          <li id="page2"><a href="#page2">2</a></li>
-          <li id="page3"><a href="#page3">3</a></li>
-          <li id="page4"><a href="#page4">4</a></li>
-          <li id="page5"><a href="#page5">5</a></li>
-          <li id="next"><a>Next<span class="arrow">&rarr;</span></a></li>
-        </ul>
+
+        <ul id="executionsPagination" class="pagination"></ul>
       </div>
     </div>
   </div><!-- /.container-fill -->
@@ -154,7 +170,7 @@
       <div class="col-xs-12">
 
         <table class="table table-striped table-bordered table-condensed table-hover"
-               id="execTable">
+               id="flowtriggerTable">
           <thead>
           <tr>
             <th>Flow Trigger Instance Id</th>
@@ -166,18 +182,11 @@
             <th class="action">Action</th>
           </tr>
           </thead>
-          <tbody id="triggerTableBody">
+          <tbody id="flowtriggerTableBody">
           </tbody>
         </table>
-        <ul id="pageSelection" class="pagination">
-          <li id="previous" class="first"><a><span class="arrow">&larr;</span>Previous</a></li>
-          <li id="page1"><a href="#page1">1</a></li>
-          <li id="page2"><a href="#page2">2</a></li>
-          <li id="page3"><a href="#page3">3</a></li>
-          <li id="page4"><a href="#page4">4</a></li>
-          <li id="page5"><a href="#page5">5</a></li>
-          <li id="next"><a>Next<span class="arrow">&rarr;</span></a></li>
-        </ul>
+
+        <ul id="flowtriggerPagination" class="pagination"></ul>
       </div>
     </div>
   </div><!-- /.container-fill -->
@@ -192,8 +201,8 @@
         <div class="col-xs-12">
           <div class="callout callout-info">
             <h4>Analyze last run</h4>
-            <p>Analyze the last run for aggregate performance statistics. Note: this may take a few
-              minutes, especially if your flow is large.</p>
+            <p>Analyze the last run for aggregate performance statistics.
+              Note: this may take a few minutes, especially if your flow is large.</p>
             <p>
               <button type="button" id="analyze-btn" class="btn btn-primary">Analyze</button>
             </p>
@@ -216,4 +225,3 @@
   </div><!-- /.container -->
   #end
 </body>
-</body>
diff --git a/azkaban-web-server/src/web/js/azkaban/view/flow.js b/azkaban-web-server/src/web/js/azkaban/view/flow.js
index 7d97353..a4ebe61 100644
--- a/azkaban-web-server/src/web/js/azkaban/view/flow.js
+++ b/azkaban-web-server/src/web/js/azkaban/view/flow.js
@@ -26,7 +26,24 @@ var handleJobMenuClick = function (action, el, pos) {
   else if (action == "openwindow") {
     window.open(requestURL);
   }
-}
+};
+
+var initPagination = function (elem, model) {
+  var totalPages = model.get("total");
+  if (!totalPages) {
+    return;
+  }
+
+  $(elem).twbsPagination({
+    totalPages: Math.ceil(totalPages / model.get("pageSize")),
+    startPage: model.get("page"),
+    initiateStartPageClick: false,
+    visiblePages: model.get("visiblePages"),
+    onPageClick: function (event, page) {
+      model.set({"page": page});
+    }
+  });
+};
 
 var flowTabView;
 azkaban.FlowTabView = Backbone.View.extend({
@@ -73,7 +90,7 @@ azkaban.FlowTabView = Backbone.View.extend({
     $("#flowtriggerView").hide();
     $("#executionsView").show();
     $('#summaryView').hide();
-    executionModel.trigger("change:view");
+    executionModel.trigger("initView");
   },
 
   handleFlowTriggerLinkClick: function () {
@@ -86,7 +103,7 @@ azkaban.FlowTabView = Backbone.View.extend({
     $("#flowtriggerView").show();
     $("#executionsView").hide();
     $('#summaryView').hide();
-    flowTriggerModel.trigger("change:view");
+    flowTriggerModel.trigger("initView");
   },
 
   handleSummaryLinkClick: function () {
@@ -104,31 +121,23 @@ azkaban.FlowTabView = Backbone.View.extend({
 
 var jobListView;
 var svgGraphView;
-var executionsView;
 
+var executionModel;
+azkaban.ExecutionModel = Backbone.Model.extend({});
+
+var executionsView;
 azkaban.ExecutionsView = Backbone.View.extend({
-  events: {
-    "click #pageSelection li": "handleChangePageSelection"
-  },
 
   initialize: function (settings) {
-    this.model.bind('change:view', this.handleChangeView, this);
+    // Interested on first tab activation only to load init data
+    this.model.once('initView', this.handleInitView, this);
     this.model.bind('render', this.render, this);
-    console.log("during initialization, pageSize: " + pageSize);
-    this.model.set({page: 1, pageSize: pageSize});
     this.model.bind('change:page', this.handlePageChange, this);
   },
 
-  render: function (evt) {
-    console.log("render");
+  render: function () {
     // Render page selections
-    var content = this.model.get("content");
-    if (content == "flow") {
-      var tbody = $("#execTableBody");
-    }
-    else {
-      var tbody = $("#triggerTableBody");
-    }
+    var tbody = $("#execTableBody");
     tbody.empty();
 
     var executions = this.model.get("executions");
@@ -137,16 +146,10 @@ azkaban.ExecutionsView = Backbone.View.extend({
 
       var tdId = document.createElement("td");
       var execA = document.createElement("a");
-      if (content == "flow") {
-        $(execA).attr("href", contextURL + "/executor?execid="
-            + executions[i].execId);
-        $(execA).text(executions[i].execId);
-      }
-      else {
-        $(execA).attr("href", contextURL + "/executor?triggerinstanceid="
-            + executions[i].instanceId);
-        $(execA).text(executions[i].instanceId);
-      }
+      $(execA).attr("href", this.model.get("executionUrl") + "?execid="
+          + executions[i].execId);
+      $(execA).text(executions[i].execId);
+
       tdId.appendChild(execA);
       row.appendChild(tdId);
 
@@ -186,12 +189,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
       var status = document.createElement("div");
       $(status).addClass("status");
       $(status).addClass(executions[i].status);
-      if (content == "flow") {
-        $(status).text(statusStringMap[executions[i].status]);
-      }
-      else {
-        $(status).text(executions[i].status);
-      }
+      $(status).text(statusStringMap[executions[i].status]);
       tdStatus.appendChild(status);
       row.appendChild(tdStatus);
 
@@ -200,140 +198,170 @@ azkaban.ExecutionsView = Backbone.View.extend({
 
       tbody.append(row);
     }
+  },
 
-    this.renderPagination(evt);
+  handleInitView: function () {
+    var model = this.model;
+    this.loadAndRenderData(function () {
+      initPagination('#executionsPagination', model);
+    });
   },
 
-  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);
+  handlePageChange: function () {
+    this.loadAndRenderData();
+  },
 
-    this.model.set({"numPages": numPages});
-    var page = this.model.get("page");
+  loadAndRenderData: function (onDataLoadedCb) {
+    var model = this.model;
+    var currentPage = model.get("page");
+    var pageSize = model.get("pageSize");
+    var requestURL = model.get("fetchFlowExecutionsUrl");
+    var requestData = {
+      "project": model.get("projectName"),
+      "flow": model.get("flowId"),
+      "ajax": "fetchFlowExecutions",
+      "start": (currentPage - 1) * pageSize,
+      "length": pageSize
+    };
 
-    //Start it off
-    $("#pageSelection .active").removeClass("active");
+    $.get(requestURL, requestData)
+    .done(function (data) {
+      model.set({
+        "executions": data.executions,
+        "total": data.total
+      });
+      model.trigger("render");
+      if (onDataLoadedCb) {
+        onDataLoadedCb();
+      }
+    })
+    .fail(function () {
+      // TODO
+    });
+  }
+});
 
-    // 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");
-    }
+var flowTriggerModel;
+azkaban.FlowTriggerModel = Backbone.Model.extend({});
 
-    // 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");
-    }
+var flowTriggersView;
+azkaban.FlowTriggersView = Backbone.View.extend({
 
-    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");
-    }
+  initialize: function (settings) {
+    // Interested on first tab activation only to load init data
+    this.model.once('initView', this.handleInitView, this);
+    this.model.bind('render', this.render, this);
+    this.model.bind('change:page', this.handlePageChange, this);
+  },
 
-    // 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;
-    }
+  render: function () {
+    // Render page selections
+    var tbody = $("#flowtriggerTableBody");
+    tbody.empty();
 
-    $("#page" + selectionPosition).addClass("active");
-    $("#page" + selectionPosition)[0].page = page;
-    var selecta = $("#page" + selectionPosition + " a");
-    selecta.text(page);
-    selecta.attr("href", "#page" + page);
+    var executions = this.model.get("executions");
+    for (var i = 0; i < executions.length; ++i) {
+      var row = document.createElement("tr");
 
-    for (var j = 0; j < 5; ++j) {
-      var realPage = startPage + j;
-      var elementId = "#page" + (j + 1);
+      var tdId = document.createElement("td");
+      var execA = document.createElement("a");
+      $(execA).attr("href", this.model.get("triggerInstanceUrl") +
+          "?triggerinstanceid=" + executions[i].instanceId);
+      $(execA).text(executions[i].instanceId);
+      tdId.appendChild(execA);
+      row.appendChild(tdId);
 
-      $(elementId)[0].page = realPage;
-      var a = $(elementId + " a");
-      a.text(realPage);
-      a.attr("href", "#page" + realPage);
-    }
-  },
+      var tdUser = document.createElement("td");
+      $(tdUser).text(executions[i].submitUser);
+      row.appendChild(tdUser);
 
-  handleChangePageSelection: function (evt) {
-    if ($(evt.currentTarget).hasClass("disabled")) {
-      return;
+      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 && executions[i].endTime != 0) {
+        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(executions[i].status);
+      tdStatus.appendChild(status);
+      row.appendChild(tdStatus);
+
+      var tdAction = document.createElement("td");
+      row.appendChild(tdAction);
+
+      tbody.append(row);
     }
-    var page = evt.currentTarget.innerText;
-    this.model.set({"page": page});
   },
 
-  handleChangeView: function (evt) {
-    if (this.init) {
-      return;
-    }
-    console.log("init");
-    this.handlePageChange(evt);
-    this.init = true;
+  handleInitView: function () {
+    var model = this.model;
+    this.loadAndRenderData(function () {
+      initPagination('#flowtriggerPagination', model);
+    });
   },
 
-  handlePageChange: function (evt) {
-    var page = this.model.get("page") - 1;
-    var pageSize = this.model.get("pageSize");
-    console.log("pageSize = " + pageSize)
-    var content = this.model.get("content");
-    if (content == 'flow') {
-      requestURL = contextURL + "/manager";
-    }
-    else {
-      requestURL = contextURL + "/flowtriggerinstance";
-    }
+  handlePageChange: function () {
+    this.loadAndRenderData();
+  },
 
+  loadAndRenderData: function (onDataLoadedCb) {
     var model = this.model;
+    var currentPage = model.get("page");
+    var pageSize = model.get("pageSize");
+    var requestURL = model.get("fetchTriggerInstancesUrl");
     var requestData = {
-      "project": projectName,
-      "flow": flowId,
-      "ajax": content == 'flow' ? "fetchFlowExecutions"
-          : "fetchTriggerInstances",
-      "start": page * pageSize,
+      "project": model.get("projectName"),
+      "flow": model.get("flowId"),
+      "ajax": "fetchTriggerInstances",
+      "start": (currentPage - 1) * pageSize,
       "length": pageSize
     };
 
-    var successHandler = function (data) {
+    $.get(requestURL, requestData)
+    .done(function (data) {
       model.set({
-        "content": content,
         "executions": data.executions,
         "total": data.total
       });
       model.trigger("render");
-    };
-    $.get(requestURL, requestData, successHandler, "json");
+      if (onDataLoadedCb) {
+        onDataLoadedCb();
+      }
+    })
+    .fail(function () {
+      // TODO
+    });
   }
 });
 
+var summaryModel;
+azkaban.SummaryModel = Backbone.Model.extend({});
+
 var summaryView;
 azkaban.SummaryView = Backbone.View.extend({
   events: {
@@ -351,20 +379,19 @@ azkaban.SummaryView = Backbone.View.extend({
   },
 
   fetchDetails: function () {
-    var requestURL = contextURL + "/manager";
+    var model = this.model;
+    var requestURL = model.get("fetchFlowDetailsUrl");
     var requestData = {
       'ajax': 'fetchflowdetails',
-      'project': projectName,
-      'flow': flowId
+      'project': model.get("projectName"),
+      'flow': model.get("flowId")
     };
 
-    var model = this.model;
-
     var successHandler = function (data) {
       console.log(data);
       model.set({
-        'jobTypes': data.jobTypes,
-        'condition': data.condition
+        jobTypes: data.jobTypes,
+        condition: data.condition
       });
       model.trigger('render');
     };
@@ -372,13 +399,14 @@ azkaban.SummaryView = Backbone.View.extend({
   },
 
   fetchSchedule: function () {
-    var requestURL = contextURL + "/schedule"
+    var model = this.model;
+    var requestURL = model.get("fetchScheduleUrl");
     var requestData = {
       'ajax': 'fetchSchedule',
-      'projectId': projectId,
-      'flowId': flowId
+      'projectId': model.get("projectId"),
+      'flowId': model.get("flowId")
     };
-    var model = this.model;
+
     var view = this;
     var successHandler = function (data) {
       model.set({'schedule': data.schedule});
@@ -394,12 +422,13 @@ azkaban.SummaryView = Backbone.View.extend({
       return;
     }
 
-    var requestURL = contextURL + "/schedule"
+    var model = this.model;
+    var requestURL = model.get("slaInfoUrl");
     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;
@@ -412,11 +441,12 @@ azkaban.SummaryView = Backbone.View.extend({
   },
 
   fetchLastRun: function () {
-    var requestURL = contextURL + "/manager";
+    var model = this.model;
+    var requestURL = model.get("fetchLastSuccessfulFlowExecutionUrl");
     var requestData = {
       'ajax': 'fetchLastSuccessfulFlowExecution',
-      'project': projectName,
-      'flow': flowId
+      'project': model.get("projectName"),
+      'flow': model.get("flowId")
     };
     var view = this;
     var successHandler = function (data) {
@@ -432,14 +462,14 @@ azkaban.SummaryView = Backbone.View.extend({
   },
 
   fetchFlowTrigger: function () {
-    var requestURL = contextURL + "/flowtrigger"
+    var model = this.model;
+    var requestURL = model.get("fetchTriggerUrl");
     var requestData = {
       'ajax': 'fetchTrigger',
-      'projectId': projectId,
-      'flowId': flowId
+      'projectId': model.get("projectId"),
+      'flowId': model.get("flowId")
     };
-    var model = this.model;
-    var view = this;
+
     var successHandler = function (data) {
       model.set({'flowtrigger': data.flowTrigger});
       model.trigger('render');
@@ -451,13 +481,14 @@ azkaban.SummaryView = Backbone.View.extend({
   },
 
   render: function (evt) {
+    var model = this.model;
     var data = {
-      projectName: projectName,
-      flowName: flowId,
-      jobTypes: this.model.get('jobTypes'),
-      condition: this.model.get('condition'),
-      schedule: this.model.get('schedule'),
-      flowtrigger: this.model.get('flowtrigger'),
+      projectName: model.get('projectName'),
+      flowName: model.get('flowId'),
+      jobTypes: model.get('jobTypes'),
+      condition: model.get('condition'),
+      schedule: model.get('schedule'),
+      flowtrigger: model.get('flowtrigger')
     };
     dust.render("flowsummary", data, function (err, out) {
       $('#summary-view-content').html(out);
@@ -467,40 +498,53 @@ azkaban.SummaryView = Backbone.View.extend({
 
 var graphModel;
 var mainSvgGraphView;
-
-var executionModel;
-azkaban.ExecutionModel = Backbone.Model.extend({});
-
-var flowTriggerModel;
-azkaban.FlowTriggerModel = Backbone.Model.extend({});
-
-var summaryModel;
-azkaban.SummaryModel = Backbone.Model.extend({});
-
 var flowStatsView;
 var flowStatsModel;
-
 var executionsTimeGraphView;
 var slaView;
 
-$(function () {
+var initFlowPage = function (settings) {
   var selected;
+
   // Execution model has to be created before the window switches the tabs.
-  executionModel = new azkaban.ExecutionModel();
-  executionModel.set("content", "flow");
+  executionModel = new azkaban.ExecutionModel({
+    page: 1,
+    pageSize: settings.pageSize,
+    visiblePages: 5,
+    projectName: settings.projectName,
+    flowId: settings.flowId,
+    executionUrl: settings.executionUrl,
+    fetchFlowExecutionsUrl: settings.fetchFlowExecutionsUrl
+  });
   executionsView = new azkaban.ExecutionsView({
     el: $('#executionsView'),
     model: executionModel
   });
 
-  flowTriggerModel = new azkaban.ExecutionModel();
-  flowTriggerModel.set("content", "trigger");
-  flowTriggerView = new azkaban.ExecutionsView({
+  flowTriggerModel = new azkaban.FlowTriggerModel({
+    page: 1,
+    pageSize: settings.pageSize,
+    visiblePages: 5,
+    projectName: settings.projectName,
+    flowId: settings.flowId,
+    triggerInstanceUrl: settings.triggerInstanceUrl,
+    fetchTriggerInstancesUrl: settings.fetchTriggerInstancesUrl
+  });
+  flowTriggersView = new azkaban.FlowTriggersView({
     el: $('#flowtriggerView'),
     model: flowTriggerModel
   });
 
-  summaryModel = new azkaban.SummaryModel();
+  summaryModel = new azkaban.SummaryModel({
+    projectId: settings.projectId,
+    projectName: settings.projectName,
+    flowId: settings.flowId,
+    fetchFlowDetailsUrl: settings.fetchFlowDetailsUrl,
+    fetchScheduleUrl: settings.fetchScheduleUrl,
+    slaInfoUrl: settings.slaInfoUrl,
+    fetchLastSuccessfulFlowExecutionUrl: settings.fetchLastSuccessfulFlowExecutionUrl,
+    fetchTriggerUrl: settings.fetchTriggerUrl
+  });
   summaryView = new azkaban.SummaryView({
     el: $('#summaryView'),
     model: summaryModel
@@ -542,24 +586,21 @@ $(function () {
 
   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,
+      project: settings.projectName,
       ajax: "executeFlow",
-      flow: flowId
+      flow: settings.flowId
     };
 
     flowExecuteDialogView.show(executingData);
   });
 
   var requestData = {
-    "project": projectName,
+    "project": settings.projectName,
     "ajax": "fetchflowgraph",
-    "flow": flowId
+    "flow": settings.flowId
   };
   var successHandler = function (data) {
     console.log("data fetched");
@@ -578,23 +619,11 @@ $(function () {
       if (hash == "#flowtriggers") {
         flowTabView.handleFlowTriggerLinkClick();
       }
-
       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");
-});
+  $.get(settings.fetchFlowGraphUrl, requestData, successHandler, "json");
+};
diff --git a/azkaban-web-server/src/web/js/jquery.twbsPagination.min.js b/azkaban-web-server/src/web/js/jquery.twbsPagination.min.js
new file mode 100755
index 0000000..315f27b
--- /dev/null
+++ b/azkaban-web-server/src/web/js/jquery.twbsPagination.min.js
@@ -0,0 +1,10 @@
+/*
+ * jQuery Bootstrap Pagination v1.4.2
+ * https://github.com/josecebe/twbs-pagination
+ *
+ * Copyright 2014-2018, Eugene Simakin <john-24@list.ru>
+ * Released under Apache-2.0 license
+ * http://apache.org/licenses/LICENSE-2.0.html
+ */
+
+!function(o,e,t,s){"use strict";var i=o.fn.twbsPagination,r=function(t,s){if(this.$element=o(t),this.options=o.extend({},o.fn.twbsPagination.defaults,s),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw new Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw new Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw new Error("Visible pages option is not correct!");if(this.options.beforePageClick instanceof Function&&this.$element.first().on("beforePage",this.options.beforePageClick),this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.options.initiateStartPageClick&&this.$element.trigger("page",1),this;if(this.options.href&&(this.options.startPage=this.getPageFromQueryString(),this.options.startPage||(this.options.startPage=1)),"UL"===("function"==typeof this.$element.prop?this.$element.prop("tagName"):this.$element.attr("tagName")))this.$listContainer=this.$element;else{var e=this.$element,i=o([]);e.each(function(t){var s=o("<ul></ul>");o(this).append(s),i.push(s[0])}),this.$listContainer=i,this.$element=i}return this.$listContainer.addClass(this.options.paginationClass),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.currentPage=this.options.startPage,this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};r.prototype={constructor:r,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(t){if(t<1||t>this.options.totalPages)throw new Error("Page is incorrect.");this.currentPage=t,this.$element.trigger("beforePage",t);var s=this.getPages(t);return this.render(s),this.setupEvents(),this.$element.trigger("page",t),s},enable:function(){this.show(this.currentPage)},disable:function(){var t=this;this.$listContainer.off("click").on("click","li",function(t){t.preventDefault()}),this.$listContainer.children().each(function(){o(this).hasClass(t.options.activeClass)||o(this).addClass(t.options.disabledClass)})},buildListItems:function(t){var s=[];if(this.options.first&&s.push(this.buildItem("first",1)),this.options.prev){var e=1<t.currentPage?t.currentPage-1:this.options.loop?this.options.totalPages:1;s.push(this.buildItem("prev",e))}for(var i=0;i<t.numeric.length;i++)s.push(this.buildItem("page",t.numeric[i]));if(this.options.next){var a=t.currentPage<this.options.totalPages?t.currentPage+1:this.options.loop?1:this.options.totalPages;s.push(this.buildItem("next",a))}return this.options.last&&s.push(this.buildItem("last",this.options.totalPages)),s},buildItem:function(t,s){var e=o("<li></li>"),i=o("<a></a>"),a=this.options[t]?this.makeText(this.options[t],s):s;return e.addClass(this.options[t+"Class"]),e.data("page",s),e.data("page-type",t),e.append(i.attr("href",this.makeHref(s)).addClass(this.options.anchorClass).html(a)),e},getPages:function(t){var s=[],e=Math.floor(this.options.visiblePages/2),i=t-e+1-this.options.visiblePages%2,a=t+e,n=this.options.visiblePages;n>this.options.totalPages&&(n=this.options.totalPages),i<=0&&(i=1,a=n),a>this.options.totalPages&&(i=this.options.totalPages-n+1,a=this.options.totalPages);for(var o=i;o<=a;)s.push(o),o++;return{currentPage:t,numeric:s}},render:function(s){var e=this;this.$listContainer.children().remove();var t=this.buildListItems(s);o.each(t,function(t,s){e.$listContainer.append(s)}),this.$listContainer.children().each(function(){var t=o(this);switch(t.data("page-type")){case"page":t.data("page")===s.currentPage&&t.addClass(e.options.activeClass);break;case"first":t.toggleClass(e.options.disabledClass,1===s.currentPage);break;case"last":t.toggleClass(e.options.disabledClass,s.currentPage===e.options.totalPages);break;case"prev":t.toggleClass(e.options.disabledClass,!e.options.loop&&1===s.currentPage);break;case"next":t.toggleClass(e.options.disabledClass,!e.options.loop&&s.currentPage===e.options.totalPages)}})},setupEvents:function(){var e=this;this.$listContainer.off("click").on("click","li",function(t){var s=o(this);if(s.hasClass(e.options.disabledClass)||s.hasClass(e.options.activeClass))return!1;!e.options.href&&t.preventDefault(),e.show(parseInt(s.data("page")))})},changeTotalPages:function(t,s){return this.options.totalPages=t,this.show(s)},makeHref:function(t){return this.options.href?this.generateQueryString(t):"#"},makeText:function(t,s){return t.replace(this.options.pageVariable,s).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(t){var s=this.getSearchString(t),e=new RegExp(this.options.pageVariable+"(=([^&#]*)|&|#|$)").exec(s);return e&&e[2]?(e=decodeURIComponent(e[2]),e=parseInt(e),isNaN(e)?null:e):null},generateQueryString:function(t,s){var e=this.getSearchString(s),i=new RegExp(this.options.pageVariable+"=*[^&#]*");return e?"?"+e.replace(i,this.options.pageVariable+"="+t):""},getSearchString:function(t){var s=t||e.location.search;return""===s?null:(0===s.indexOf("?")&&(s=s.substr(1)),s)},getCurrentPage:function(){return this.currentPage},getTotalPages:function(){return this.options.totalPages}},o.fn.twbsPagination=function(t){var s,e=Array.prototype.slice.call(arguments,1),i=o(this),a=i.data("twbs-pagination"),n="object"==typeof t?t:{};return a||i.data("twbs-pagination",a=new r(this,n)),"string"==typeof t&&(s=a[t].apply(a,e)),void 0===s?i:s},o.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,beforePageClick:null,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},o.fn.twbsPagination.Constructor=r,o.fn.twbsPagination.noConflict=function(){return o.fn.twbsPagination=i,this},o.fn.twbsPagination.version="1.4.2"}(window.jQuery,window,document);
\ No newline at end of file