azkaban-aplcache

flow trigger instance UI (#1637) This PR added flow trigger

2/12/2018 4:38:53 PM
3.42.0

Details

diff --git a/azkaban-web-server/src/main/java/azkaban/webapp/AzkabanWebServer.java b/azkaban-web-server/src/main/java/azkaban/webapp/AzkabanWebServer.java
index 3cfb8af..f1cfc51 100644
--- a/azkaban-web-server/src/main/java/azkaban/webapp/AzkabanWebServer.java
+++ b/azkaban-web-server/src/main/java/azkaban/webapp/AzkabanWebServer.java
@@ -54,6 +54,7 @@ import azkaban.webapp.plugin.TriggerPlugin;
 import azkaban.webapp.plugin.ViewerPlugin;
 import azkaban.webapp.servlet.AbstractAzkabanServlet;
 import azkaban.webapp.servlet.ExecutorServlet;
+import azkaban.webapp.servlet.FlowTriggerInstanceServlet;
 import azkaban.webapp.servlet.HistoryServlet;
 import azkaban.webapp.servlet.IndexRedirectServlet;
 import azkaban.webapp.servlet.JMXHttpServlet;
@@ -506,6 +507,7 @@ public class AzkabanWebServer extends AzkabanServer {
     root.addServlet(new ServletHolder(new StatsServlet()), "/stats");
     root.addServlet(new ServletHolder(new StatusServlet(this.statusService)), "/status");
     root.addServlet(new ServletHolder(new NoteServlet()), "/notes");
+    root.addServlet(new ServletHolder(new FlowTriggerInstanceServlet()), "/flowtriggerinstance");
 
     final ServletHolder restliHolder = new ServletHolder(new RestliServlet());
     restliHolder.setInitParameter("resourcePackages", "azkaban.restli");
diff --git a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/FlowTriggerInstanceServlet.java b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/FlowTriggerInstanceServlet.java
new file mode 100644
index 0000000..aa6caf2
--- /dev/null
+++ b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/FlowTriggerInstanceServlet.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2018 LinkedIn Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package azkaban.webapp.servlet;
+
+import azkaban.flowtrigger.CancellationCause;
+import azkaban.flowtrigger.DependencyInstance;
+import azkaban.flowtrigger.FlowTriggerService;
+import azkaban.flowtrigger.TriggerInstance;
+import azkaban.project.Project;
+import azkaban.project.ProjectManager;
+import azkaban.server.session.Session;
+import azkaban.user.Permission.Type;
+import azkaban.webapp.AzkabanWebServer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.log4j.Logger;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+public class FlowTriggerInstanceServlet extends LoginAbstractAzkabanServlet {
+
+  private static final long serialVersionUID = 1L;
+  private static final Logger logger = Logger.getLogger(FlowTriggerInstanceServlet.class);
+  private FlowTriggerService triggerService;
+  private ProjectManager projectManager;
+
+  @Override
+  public void init(final ServletConfig config) throws ServletException {
+    super.init(config);
+    final AzkabanWebServer server = (AzkabanWebServer) getApplication();
+    this.triggerService = server.getFlowTriggerService();
+    this.projectManager = server.getProjectManager();
+  }
+
+  @Override
+  protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp,
+      final Session session) throws ServletException, IOException {
+    if (hasParam(req, "ajax")) {
+      handleAJAXAction(req, resp, session);
+    } else {
+      handlePage(req, resp, session);
+    }
+  }
+
+  private void handlePage(final HttpServletRequest req,
+      final HttpServletResponse resp, final Session session) {
+    final Page page =
+        newPage(req, resp, session,
+            "azkaban/webapp/servlet/velocity/executingflowtriggerspage.vm");
+
+    page.add("runningTriggers", this.triggerService.getRunningTriggers());
+    page.add("recentTriggers", this.triggerService.getRecentlyFinished());
+
+    page.add("vmutils", new ExecutorVMHelper());
+    page.render();
+  }
+
+  private void handleAJAXAction(final HttpServletRequest req,
+      final HttpServletResponse resp, final Session session) throws ServletException,
+      IOException {
+    final HashMap<String, Object> ret = new HashMap<>();
+    final String ajaxName = getParam(req, "ajax");
+
+    if (ajaxName.equals("fetchRunningTriggers")) {
+      ajaxFetchRunningTriggerInstances(ret);
+    } else if (ajaxName.equals("killRunningTrigger")) {
+      if (hasParam(req, "id")) {
+        final String triggerInstanceId = getParam(req, "id");
+        ajaxKillTriggerInstance(triggerInstanceId, session, ret);
+      } else {
+        ret.put("error", "please specify a valid running trigger instance id");
+      }
+    } else if (ajaxName.equals("showTriggerProperties")) {
+      if (hasParam(req, "id")) {
+        final String triggerInstanceId = getParam(req, "id");
+        loadTriggerProperties(triggerInstanceId, ret);
+      } else {
+        ret.put("error", "please specify a valid running trigger instance id");
+      }
+    }
+
+    if (ret != null) {
+      this.writeJSON(resp, ret);
+    }
+  }
+
+  private void loadTriggerProperties(final String triggerInstanceId,
+      final HashMap<String, Object> ret) {
+    final TriggerInstance triggerInstance = this.triggerService
+        .findTriggerInstanceById(triggerInstanceId);
+    if (triggerInstance != null) {
+      ret.put("triggerProperties", triggerInstance.getFlowTrigger().toString());
+    } else {
+      ret.put("error", "the trigger instance doesn't exist");
+    }
+  }
+
+  private void ajaxKillTriggerInstance(final String triggerInstanceId, final Session session,
+      final HashMap<String, Object> ret) {
+    final TriggerInstance triggerInst = this.triggerService
+        .findRunningTriggerInstById(triggerInstanceId);
+    if (triggerInst != null) {
+      if (hasPermission(triggerInst.getProject(), session.getUser(), Type.EXECUTE)) {
+        this.triggerService.cancel(triggerInst, CancellationCause.MANUAL);
+      } else {
+        ret.put("error", "no permission to kill the trigger");
+      }
+    } else {
+      ret.put("error", "the trigger doesn't exist, might already finished or cancelled");
+    }
+  }
+
+  private void ajaxFetchRunningTriggerInstances(final HashMap<String, Object> ret) throws
+      ServletException {
+    final Collection<TriggerInstance> triggerInstanceList = this.triggerService
+        .getRunningTriggers();
+
+    final List<HashMap<String, Object>> output = new ArrayList<>();
+    ret.put("items", output);
+
+    for (final TriggerInstance triggerInstance : triggerInstanceList) {
+      writeTriggerInstancesData(output, triggerInstance);
+    }
+  }
+
+  private void writeTriggerInstancesData(final List<HashMap<String, Object>> output,
+      final TriggerInstance triggerInst) {
+
+    final HashMap<String, Object> data = new HashMap<>();
+    data.put("id", triggerInst.getId());
+    data.put("starttime", triggerInst.getStartTime());
+    data.put("endtime", triggerInst.getEndTime());
+    data.put("status", triggerInst.getStatus());
+    data.put("flowExecutionId", triggerInst.getFlowExecId());
+    data.put("submitUser", triggerInst.getSubmitUser());
+    data.put("flowTriggerConfig", triggerInst.getFlowTrigger());
+    final List<Map<String, Object>> dependencyOutput = new ArrayList<>();
+    for (final DependencyInstance depInst : triggerInst.getDepInstances()) {
+      final Map<String, Object> depMap = new HashMap<>();
+      depMap.put("dependencyName", depInst.getDepName());
+      depMap.put("dependencyStarttime", depInst.getStartTime());
+      depMap.put("dependencyEndtime", depInst.getEndTime());
+      depMap.put("dependencyStatus", depInst.getStatus());
+      depMap.put("dependencyConfig", depInst.getTriggerInstance().getFlowTrigger()
+          .getDependencyByName
+              (depInst.getDepName()));
+      dependencyOutput.add(depMap);
+    }
+    data.put("dependencies", dependencyOutput);
+    output.add(data);
+  }
+
+  @Override
+  protected void handlePost(final HttpServletRequest req, final HttpServletResponse resp,
+      final Session session) throws ServletException, IOException {
+    if (hasParam(req, "ajax")) {
+      handleAJAXAction(req, resp, session);
+    }
+  }
+
+  /**
+   * @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(final String cronTimezone) {
+    if (cronTimezone != null && cronTimezone.equals("UTC")) {
+      return DateTimeZone.UTC;
+    }
+
+    return DateTimeZone.getDefault();
+  }
+
+  private DateTime getPresentTimeByTimezone(final DateTimeZone timezone) {
+    return new DateTime(timezone);
+  }
+
+  public class ExecutorVMHelper {
+
+    public String getProjectName(final int id) {
+      final Project project = FlowTriggerInstanceServlet.this.projectManager.getProject(id);
+      if (project == null) {
+        return String.valueOf(id);
+      }
+
+      return project.getName();
+    }
+  }
+}
diff --git a/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/executingflowtriggerspage.vm b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/executingflowtriggerspage.vm
new file mode 100644
index 0000000..5d503f2
--- /dev/null
+++ b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/executingflowtriggerspage.vm
@@ -0,0 +1,397 @@
+#*
+ * Copyright 2018 LinkedIn Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+*#
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+  #parse("azkaban/webapp/servlet/velocity/style.vm")
+  #parse("azkaban/webapp/servlet/velocity/javascript.vm")
+  <script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
+  #
+  <script type="text/javascript" src="${context}/js/azkaban/view/executions.js"></script>
+  <script type="text/javascript" src="${context}/js/jquery/jquery.tablesorter.js"></script>
+  <script type="text/javascript">
+    var contextURL = "${context}";
+    var currentTime = ${currentTime};
+    var timezone = "${timezone}";
+    var errorMessage = null;
+    var successMessage = null;
+
+    $(document).ready(function () {
+      var jobTable = $("#executingJobs");
+      jobTable.tablesorter();
+    });
+  </script>
+  <script>
+    $(function () {
+      $('tr.parent')
+      .css("cursor", "pointer")
+      .attr("title", "Click to expand/collapse")
+      .click(function () {
+        $(this).siblings('.child-' + this.id).toggle();
+      });
+      $('tr[class^=child-]').hide().children('tr');
+
+    });
+  </script>
+  <script type="text/javascript">
+    function killTrigger(id) {
+      var requestURL = document.location.href.replace("#currently-running", "");
+      var requestData = {"id": id, "ajax": "killRunningTrigger"};
+      var successHandler = function (data) {
+        console.log("cancel clicked");
+        if (data.error) {
+          showDialog("Error", data.error);
+        }
+        else {
+          showDialog("Killed", "Trigger has been killed.");
+
+        }
+      };
+      ajaxCall(requestURL, requestData, successHandler);
+    }
+
+    function showTriggerProperties(id) {
+      var requestURL = document.location.href.replace("#currently-running", "");
+      var requestData = {"id": id, "ajax": "showTriggerProperties"};
+      var successHandler = function (data) {
+        console.log("cancel clicked");
+        if (data.error) {
+          showDialog("Error", data.error);
+        }
+        else {
+          //showDialog("flow trigger properties", data.triggerProperties);
+          alert(data.triggerProperties);
+        }
+      };
+      //alert(requestURL);
+      ajaxCall(requestURL, requestData, successHandler);
+    }
+  </script>
+</head>
+<body>
+
+  #set ($current_page="executing")
+  #parse ("azkaban/webapp/servlet/velocity/nav.vm")
+
+## Page header.
+
+<div class="az-page-header">
+  <div class="container-full">
+    <h1><a href="${context}/executor">Executing Triggers</a></h1>
+  </div>
+</div>
+
+<div class="container-full">
+
+  #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
+
+## Page Content
+
+  <ul class="nav nav-tabs nav-sm" id="header-tabs">
+    <li id="currently-running-view-link"><a href="#currently-running">Trigger Executions</a></li>
+    <li id="recently-finished-view-link"><a href="#recently-finished">Recently Finished</a></li>
+  </ul>
+
+  <div class="row" id="currently-running-view">
+    <div class="col-xs-12">
+      <table id="executingJobs"
+             class="table table-striped table-bordered table-hover table-condensed executions-table">
+        <thead>
+        <tr>
+          <th>#</th>
+          <th class="execid">Trigger Instance Id</th>
+          <th>Flow</th>
+          <th>Project</th>
+          <th>Submitted by</th>
+          <th class="date">Start time</th>
+          <th class="date">End time</th>
+          <th class="status">Status</th>
+          <th class="elapse">Elapsed</th>
+          <th class="flowExecId">Triggered Flow Execution Id</th>
+          <th class="config">Trigger Properties</th>
+          <th class="action">Action</th>
+        </tr>
+        </thead>
+        <tbody>
+
+          #if ( !$null.isNull(${runningTriggers}))
+            #foreach ($trigger in $runningTriggers)
+            <tr class="parent" id=${trigger.getId()}>
+              <td class="tb-name">
+                $velocityCount
+              </td>
+              <td>
+                #if (${trigger.getId()})
+                  ${trigger.getId()}
+                #else
+                  -
+                #end
+              </td>
+            #*todo chengren311: keep result of vmutils.getProjectName as a variable *#
+              <td><a
+                  href="${context}/manager?project=$vmutils.getProjectName(${trigger.getProject().getId()})&flow=${trigger.getFlowId()}">${trigger.getFlowId()}</a>
+              </td>
+              <td>
+                <a href="${context}/manager?project=$vmutils.getProjectName(${trigger.getProject().getId()})">$vmutils.getProjectName(${trigger.getProject().getId()})</a>
+              </td>
+
+              <td>${trigger.getSubmitUser()}</td>
+            #*todo chengren311: verify utils.formatDate will convert to user's timezone *#
+              <td>$utils.formatDate(${trigger.getStartTime().getTime()})</td>
+              #if (${trigger.getEndTime()})
+                <td>${trigger.getEndTime()}</td>
+              #else
+                <td>-</td>
+              #end
+              <td>${trigger.getStatus()}</td>
+              #if (${trigger.getStatus()} != "RUNNING" && ${trigger.getStatus()} != "CANCELLING")
+                <td>$utils.formatDuration(${trigger.getStartTime().getTime()}, ${trigger.getEndTime().getTime()})
+                </td>
+              #else
+                <td>$utils.formatDuration(${trigger.getStartTime().getTime()}, ${utils.currentTimestamp()})
+                </td>
+              #end
+              #if (${trigger.getFlowExecId()} != "-1")
+                <td><a
+                    href="${context}/executor?execid=${trigger.getFlowExecId()}">${trigger.getFlowExecId()}</a>
+                </td>
+              #else
+                <td>-</td>
+              #end
+
+              <td>
+                <button type="button" id="showTriggerProperties" class="btn btn-danger btn-sm"
+                        onclick="showTriggerProperties('${trigger.getId()}')">
+                  Trigger Config
+                </button>
+              </td>
+
+              <td>
+                #if (${trigger.getStatus()} == "RUNNING")
+                  <button type="button" id="cancelbtn" class="btn btn-danger btn-sm"
+                          onclick="killTrigger('${trigger.getId()}')">Kill
+                  </button>
+                #else
+                  -
+                #end
+              </td>
+
+            <tr class="child-${trigger.getId()}">
+              <td>&nbsp;</td>
+              <td>Name</td>
+              <td>Start time</td>
+              <td>End time</td>
+              <td>Status</td>
+              <td>Elapsed time</td>
+            </tr>
+              #foreach ($dep in $trigger.getDepInstances())
+              <tr class="child-${trigger.getId()}">
+                <td>&nbsp;</td>
+                <td>${dep.getDepName()}</td>
+                <td>$utils.formatDate(${dep.getStartTime().getTime()})</td>
+                #if (${dep.getEndTime()})
+                  <td>${dep.getEndTime()}</td>
+                #else
+                  <td>-</td>
+                #end
+                <td>${dep.getStatus()}</td>
+                #if (${dep.getEndTime()})
+                  <td>$utils.formatDuration(${dep.getStartTime().getTime()}, ${dep.getEndTime().getTime()})
+                  </td>
+                #else
+                  <td>$utils.formatDuration(${dep.getStartTime().getTime()}, ${utils.currentTimestamp()})
+                  </td>
+                #end
+              </tr>
+              #end
+
+            #*
+            <td>
+              <a href="${context}/manager?project=$vmutils.getProjectName(${flow.getFirst().projectId})"
+            >$vmutils.getProjectName(${flow.getFirst().projectId})</a>
+            </td>
+            <td>${flow.getFirst().submitUser}</td>
+            <td>${flow.getFirst().proxyUsers}</td>
+            <td>$utils.formatDate(${flow.getFirst().startTime})</td>
+            <td>$utils.formatDate(${flow.getFirst().endTime})</td>
+            <td>$utils.formatDuration(${flow.getFirst().startTime}, ${flow.getFirst().endTime})</td>
+            <td>
+              <div
+                  class="status ${flow.getFirst().status}">$utils.formatStatus(${flow.getFirst().status})</div>
+            </td>
+            <td>
+              <button type="button" id="cancelbtn" class="btn btn-danger btn-sm"
+                              onclick="killFlow(${flow.getFirst().executionId})">Kill
+              </button>
+            </td>
+            *#
+            </tr>
+            #end
+          #else
+          <tr>
+            <td colspan="10">No Executing Flows</td>
+          </tr>
+          #end
+        </tbody>
+      </table>
+    </div><!-- /col-xs-12 -->
+  </div><!-- /row -->
+
+  <div class="row" id="recently-finished-view">
+    <div class="col-xs-12">
+      <table id="recentlyFinished"
+             class="table table-striped table-bordered table-hover table-condensed executions-table">
+        <thead>
+        <tr>
+          <th>#</th>
+          <th class="execid">Trigger Instance Id</th>
+          <th>Flow</th>
+          <th>Project</th>
+          <th>Submitted by</th>
+          <th class="date">Start time</th>
+          <th class="date">End time</th>
+          <th class="status">Status</th>
+          <th class="elapse">Elapsed</th>
+          <th class="flowExecId">Triggered Flow Execution Id</th>
+          <th class="config">Trigger Properties</th>
+          <th class="action">Action</th>
+        </tr>
+        </thead>
+        <tbody>
+
+          #if ( !$null.isNull(${recentTriggers}))
+            #foreach ($trigger in $recentTriggers)
+            <tr class="parent" id=${trigger.getId()}>
+              <td class="tb-name">
+                $velocityCount
+              </td>
+              <td>
+                #if (${trigger.getId()})
+                  ${trigger.getId()}
+                #else
+                  -
+                #end
+              </td>
+            #*todo chengren311: keep result of vmutils.getProjectName as a variable *#
+              <td><a
+                  href="${context}/manager?project=$vmutils.getProjectName(${trigger.getProject().getId()})&flow=${trigger.getFlowId()}">${trigger.getFlowId()}</a>
+              </td>
+              <td>
+                <a href="${context}/manager?project=$vmutils.getProjectName(${trigger.getProject().getId()})">$vmutils.getProjectName(${trigger.getProject().getId()})</a>
+              </td>
+
+              <td>${trigger.getSubmitUser()}</td>
+            #*todo chengren311: verify utils.formatDate will convert to user's timezone *#
+              <td>$utils.formatDate(${trigger.getStartTime().getTime()})</td>
+              #if (${trigger.getEndTime()})
+                <td>${trigger.getEndTime()}</td>
+              #else
+                <td>-</td>
+              #end
+              <td>${trigger.getStatus()}</td>
+              #if (${trigger.getStatus()} != "RUNNING" && ${trigger.getStatus()} != "CANCELLING")
+                <td>$utils.formatDuration(${trigger.getStartTime().getTime()}, ${trigger.getEndTime().getTime()})
+                </td>
+              #else
+                <td>$utils.formatDuration(${trigger.getStartTime().getTime()}, ${utils.currentTimestamp()})
+                </td>
+              #end
+              #if (${trigger.getFlowExecId()} != "-1")
+                <td><a
+                    href="${context}/executor?execid=${trigger.getFlowExecId()}">${trigger.getFlowExecId()}</a>
+                </td>
+              #else
+                <td>-</td>
+              #end
+
+              <td>
+                <button type="button" id="showTriggerProperties" class="btn btn-danger btn-sm"
+                        onclick="showTriggerProperties('${trigger.getId()}')">
+                  Trigger Config
+                </button>
+              </td>
+
+              <td>
+                -
+              </td>
+
+            <tr class="child-${trigger.getId()}">
+              <td>&nbsp;</td>
+              <td>Name</td>
+              <td>Start time</td>
+              <td>End time</td>
+              <td>Status</td>
+              <td>Cancellation Cause</td>
+              <td>Elapsed time</td>
+            </tr>
+              #foreach ($dep in $trigger.getDepInstances())
+              <tr class="child-${trigger.getId()}">
+                <td>&nbsp;</td>
+                <td>${dep.getDepName()}</td>
+                <td>$utils.formatDate(${dep.getStartTime().getTime()})</td>
+                #if (${dep.getEndTime()})
+                  <td>${dep.getEndTime()}</td>
+                #else
+                  <td>-</td>
+                #end
+                <td>${dep.getStatus()}</td>
+                <td>${dep.getCancellationCause()}</td>
+                #if (${dep.getEndTime()})
+                  <td>$utils.formatDuration(${dep.getStartTime().getTime()}, ${dep.getEndTime().getTime()})
+                  </td>
+                #else
+                  <td>$utils.formatDuration(${dep.getStartTime().getTime()}, ${utils.currentTimestamp()})
+                  </td>
+                #end
+              </tr>
+              #end
+
+            </tr>
+            #end
+          #else
+          <tr>
+            <td colspan="10">No Executing Flows</td>
+          </tr>
+          #end
+        </tbody>
+      </table>
+    </div><!-- /col-xs-12 -->
+  </div><!-- /row -->
+
+  <div class="modal" id="messageDialog">
+    <div class="modal-dialog">
+      <div class="modal-content">
+        <div class="modal-header" id="messageTitle">
+          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;
+          </button>
+          <h4 class="modal-title">Error</h4>
+        </div>
+        <div class="modal-body" id="messageDiv">
+          <p id="messageBox"></p>
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn btn-primary" data-dismiss="modal"
+                  onclick="window.location.reload(true);">Dismiss
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</div><!-- /container-full -->
+</body>
+</html>