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 8aa7acc..45d9925 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.servlet.ExecutorServlet;
import azkaban.webapp.servlet.HistoryServlet;
import azkaban.webapp.servlet.IndexRedirectServlet;
import azkaban.webapp.servlet.JMXHttpServlet;
+import azkaban.webapp.servlet.NoteServlet;
import azkaban.webapp.servlet.ProjectManagerServlet;
import azkaban.webapp.servlet.ProjectServlet;
import azkaban.webapp.servlet.ScheduleServlet;
@@ -126,7 +127,7 @@ public class AzkabanWebServer extends AzkabanServer {
@Deprecated
private static AzkabanWebServer app;
-
+
private final VelocityEngine velocityEngine;
private final StatusService statusService;
private final Server server;
@@ -476,6 +477,7 @@ public class AzkabanWebServer extends AzkabanServer {
root.addServlet(new ServletHolder(new TriggerManagerServlet()), "/triggers");
root.addServlet(new ServletHolder(new StatsServlet()), "/stats");
root.addServlet(new ServletHolder(new StatusServlet(this.statusService)), "/status");
+ root.addServlet(new ServletHolder(new NoteServlet()), "/notes");
final ServletHolder restliHolder = new ServletHolder(new RestliServlet());
restliHolder.setInitParameter("resourcePackages", "azkaban.restli");
diff --git a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index 807ee0e..673abee 100644
--- a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -285,6 +285,9 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
page.add("azkaban_name", this.name);
page.add("azkaban_label", this.label);
page.add("azkaban_color", this.color);
+ page.add("note_type", NoteServlet.type);
+ page.add("note_message", NoteServlet.message);
+ page.add("note_url", NoteServlet.url);
page.add("utils", utils);
page.add("timezone", TimeZone.getDefault().getID());
page.add("currentTime", (new DateTime()).getMillis());
@@ -331,6 +334,9 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
page.add("azkaban_name", this.name);
page.add("azkaban_label", this.label);
page.add("azkaban_color", this.color);
+ page.add("note_type", NoteServlet.type);
+ page.add("note_message", NoteServlet.message);
+ page.add("note_url", NoteServlet.url);
page.add("timezone", TimeZone.getDefault().getID());
page.add("currentTime", (new DateTime()).getMillis());
page.add("context", req.getContextPath());
diff --git a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/NoteServlet.java b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/NoteServlet.java
new file mode 100644
index 0000000..10fdf13
--- /dev/null
+++ b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/NoteServlet.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 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.server.HttpRequestUtils;
+import azkaban.server.session.Session;
+import azkaban.user.Permission.Type;
+import azkaban.user.User;
+import azkaban.webapp.AzkabanWebServer;
+import java.io.IOException;
+import java.util.HashMap;
+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;
+
+public class NoteServlet extends LoginAbstractAzkabanServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger logger = Logger .getLogger(NoteServlet.class);
+
+ public static String type = null;
+ public static String message = null;
+ public static String url= null;
+ private AzkabanWebServer server;
+
+ @Override
+ public void init(final ServletConfig config) throws ServletException {
+ super.init(config);
+ this.server = (AzkabanWebServer) getApplication();
+ }
+
+ @Override
+ protected void handleGet(final HttpServletRequest req, final HttpServletResponse resp,
+ final Session session) throws ServletException, IOException {
+ if(isAdmin(session.getUser())) {
+ handleNotePageLoad(req, resp, session);
+ } else {
+ warningNonAdminUsers(resp, "The requested user doesn't have admin permission");
+ }
+ }
+
+ private void warningNonAdminUsers(final HttpServletResponse resp, final String message) throws IOException {
+ final HashMap<String, Object> ret = new HashMap<>();
+ ret.put("error", message);
+ this.writeJSON(resp, ret);
+ }
+
+ private void handleNotePageLoad(final HttpServletRequest req,
+ final HttpServletResponse resp, final Session session) throws ServletException,
+ IOException {
+
+ final Page page = newPage(req, resp, session,
+ "azkaban/webapp/servlet/velocity/notepage.vm");
+
+ page.add("note_type", type);
+ page.add("note_message", message);
+ page.add("note_url", url);
+ page.render();
+ }
+
+ @Override
+ protected void handlePost(final HttpServletRequest req, final HttpServletResponse resp,
+ final Session session) throws ServletException, IOException {
+ if (isAdmin(session.getUser()) && hasParam(req, "ajax")) {
+ handleAJAXAction(req, resp, session);
+ } else {
+ warningNonAdminUsers(resp, "The requested user doesn't have admin permission, "
+ + "Or the HTTP request doesn't include ajax.");
+ }
+ }
+
+ 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");
+ try {
+ if (ajaxName.equals("addNote")) {
+ ajaxAddNotes(req, ret);
+ } else if (ajaxName.equals("removeNote")){
+ ajaxRemoveNotes(ret);
+ } else {
+ ret.put("error", "Can not find the ajax operation");
+ }
+ } catch (final Exception e) {
+ ret.put("error", e.getMessage());
+ }
+ this.writeJSON(resp, ret);
+ }
+
+ private void ajaxAddNotes(final HttpServletRequest req,
+ final Map<String, Object> ret) throws ServletException {
+ type = getParam(req, "type");
+ message= getParam(req, "message");
+ url = getParam(req, "url");
+ logger.info("receive note message. Type: " + type + " message: " + message + " url: " + url);
+ ret.put("status", "success");
+ }
+
+ private void ajaxRemoveNotes(final Map<String, Object> ret) throws ServletException {
+ type = null;
+ message= null;
+ url = null;
+ logger.info("removing note from memory.");
+ ret.put("status", "success");
+ }
+
+ private boolean isAdmin(final User user) {
+ return HttpRequestUtils.hasPermission(this.server.getUserManager(), user, Type.ADMIN);
+ }
+}
diff --git a/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/nav.vm b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/nav.vm
index 042c1b8..5881745 100644
--- a/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/nav.vm
+++ b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/nav.vm
@@ -14,11 +14,61 @@
* the License.
*#
+<style type="text/css">
+ #banner {
+ width: 100%;
+ background: orange;
+ height: 70px;
+ position: float;
+ top: 0;
+ display: none;
+ left: 0;
+ text-align: center;
+ line-height: 70px;
+ font-weight: bold;
+ }
+
+ #banner span:hover {
+ color: red;
+ cursor: pointer;
+ }
+</style>
<script type="text/javascript">
- function navMenuClick(url) {
- window.location.href = url;
- }
+ function navMenuClick(url) {
+ window.location.href = url;
+ }
+
+ $(function(){
+
+ if( $("#banner").data("id").indexOf('$') == -1 && $('#banner').is(':empty') == false) {
+ $('#banner').attr('Target', '_blank');
+ $("#banner").on("click", function(){
+ window.open($("#banner").data("url"));
+ });
+
+ $("#banner").slideDown(function() {
+ if($("#banner").data("id") == "Warning") {
+ $('#bannerText').append('<img ' + 'src=' + '"images/warning.png"' + ' width="50" height="50" ' + ' style=' + '"display:inline;" >' + ' </img>');
+ $('#bannerText').append('<span style="font-size: 150%;" >' + $("#banner").data("message") + '</span>');
+ } else if ($("#banner").data("id") == "Action Required") {
+ $('#bannerText').append('<img ' + 'src=' + '"images/warning.png"' + ' width="50" height="50" ' + ' style=' + '"display:inline;" >' + ' </img>');
+ $('#bannerText').append('<span style="font-size: 150%;" >' + $("#banner").data("id") + ": " + '</span>');
+ $('#bannerText').append('<span style="font-size: 150%;" >' + $("#banner").data("message") + '</span>');
+ }
+ // The banner will side up after 30 seconds
+ setTimeout(function() {
+ $("#banner").slideUp();
+ }, 30000);
+ });
+ }
+ });
</script>
+
+<div id='banner' data-id="$note_type" data-message="$note_message" data-url="$note_url" >
+ <div id="bannerText">
+ </div>
+</div>
+
<div class="navbar navbar-inverse navbar-static-top">
<div class="container-full">
<div class="navbar-header">
diff --git a/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/notepage.vm b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/notepage.vm
new file mode 100644
index 0000000..29142e5
--- /dev/null
+++ b/azkaban-web-server/src/main/resources/azkaban/webapp/servlet/velocity/notepage.vm
@@ -0,0 +1,103 @@
+#*
+ * Copyright 2017 LinkedIn, Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+*#
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+ #parse("azkaban/webapp/servlet/velocity/style.vm")
+ #parse("azkaban/webapp/servlet/velocity/javascript.vm")
+
+ <link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
+ <link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui.css" />
+
+ <script type="text/javascript" src="${context}/js/jqueryui/jquery-ui-1.10.1.custom.js"></script>
+ <script type="text/javascript" src="${context}/js/azkaban/view/note.js"></script>
+ <script type="text/javascript">
+ var contextURL = "${context}";
+ var currentTime = ${currentTime};
+ var timezone = "${timezone}";
+ var errorMessage = null;
+ var successMessage = null;
+ </script>
+</head>
+<body>
+
+<style type="text/css">
+ .show-and-hide-true {
+ display:none;
+ }
+</style>
+
+ #set ($current_page="notes")
+ #parse ("azkaban/webapp/servlet/velocity/nav.vm")
+
+ #if ($errorMsg)
+ #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
+ #else
+
+ <div class="az-page-header">
+ <div class="container-full">
+ <h1>Notification System</h1>
+ </div>
+ </div>
+
+ <div class="container">
+ <form class="form-horizontal" role="form">
+
+ <div class="form-group">
+ <label class="control-label col-sm-3">Type: </label>
+ <div class="col-sm-6">
+ <div class="row show-and-hide-content">
+ <div class="col-sm-4">
+ <label class="radio-inline">
+ <input type="radio" name='note-type' value="Warning">Warning
+ </label>
+ </div>
+ <div class="col-sm-4">
+ <label class="radio-inline">
+ <input type="radio" name='note-type' value="Action Required">Action Required
+ </label>
+ </div>
+ </div>
+ </div>
+ </div> <!-- /.form-group -->
+
+ <div class="form-group">
+ <label for="firstName" class="col-sm-3 control-label">Message: </label>
+ <div class="col-sm-6">
+ <input type="text" id="message" placeholder="Message" class="form-control" autofocus>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="email" class="col-sm-3 control-label">Redirect: </label>
+ <div class="col-sm-9">
+ <input type="text" id="url" placeholder="URL" class="form-control">
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-sm-3 col-sm-offset-3">
+ <button type="submit" id="submit-button" class="btn btn-primary btn-block">Submit Note</button>
+ </div>
+ <div class="col-sm-2 col-sm-offset-4">
+ <button type="submit" id="clear-button" class="btn btn-warning btn-block">Clear Note</button>
+ </div>
+ </div>
+ </form> <!-- /form -->
+ </div> <!-- ./container -->
+ #end
+</body>
+</html>
diff --git a/azkaban-web-server/src/web/images/warning.png b/azkaban-web-server/src/web/images/warning.png
new file mode 100644
index 0000000..3f4d539
Binary files /dev/null and b/azkaban-web-server/src/web/images/warning.png differ
diff --git a/azkaban-web-server/src/web/js/azkaban/view/note.js b/azkaban-web-server/src/web/js/azkaban/view/note.js
new file mode 100644
index 0000000..018af48
--- /dev/null
+++ b/azkaban-web-server/src/web/js/azkaban/view/note.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 LinkedIn Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+$(function() {
+
+ $("#submit-button").click(function(){
+ console.log("======create note=====")
+ var radioValue = $("input[name='note-type']:checked").val();
+ var message = $('#message').val();
+ var url = $('#url').val();
+
+ var triggerURL = "/notes";
+ var redirectURL = "/notes";
+ var requestData = {"ajax": "addNote", "type": radioValue, "message": message, "url": url};
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#errorMsg').text(data.error);
+ }
+ else {
+ window.location = redirectURL;
+ }
+ };
+ $.post(triggerURL, requestData, successHandler, "json");
+ });
+
+ $("#clear-button").click(function(){
+
+ console.log("======form clear=====")
+ var requestData = {"ajax": "removeNote"};
+ var triggerURL = "/notes";
+ var redirectURL = "/notes";
+ var successHandler = function(data) {
+ if (data.error) {
+ $('#errorMsg').text(data.error);
+ }
+ else {
+ window.location = redirectURL;
+ }
+ };
+ $.post(triggerURL, requestData, successHandler, "json");
+ });
+});