From dfbe6d6ead54989fb59f039b0d365399f8f81c6d Mon Sep 17 00:00:00 2001 From: Shawn Pearce Date: Thu, 2 Jan 2014 08:01:52 -0800 Subject: [PATCH] Add MessageOfTheDay extension point for plugins Allow plugins to contribute messages to the UI during initial page load. Gerrit displays the messages in a butter bar at the top of the page and allows users to hide them with the "Dismiss" button. Messages are hidden per-browser using a cookie named after the message id, set to expire at a date supplied by the server. After this date the same message could redisplay if the server sends the same message again. Change-Id: I0bcca845f501cbeb8c31356fff398c3adb43099a --- .../gerrit/common/data/HostPageData.java | 8 ++ .../systemstatus/MessageOfTheDay.java | 69 ++++++++++++++ .../java/com/google/gerrit/client/Gerrit.java | 3 + .../gerrit/client/MessageOfTheDayBar.java | 89 +++++++++++++++++++ .../gerrit/client/MessageOfTheDayBar.ui.xml | 71 +++++++++++++++ .../gerrit/httpd/raw/HostPageServlet.java | 26 ++++++ .../server/config/GerritGlobalModule.java | 2 + 7 files changed, 268 insertions(+) create mode 100644 gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java index f143405ab0..323af236c4 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java @@ -17,6 +17,7 @@ package com.google.gerrit.common.data; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountDiffPreference; +import java.util.Date; import java.util.List; /** Data sent as part of the host page, to bootstrap the UI. */ @@ -28,6 +29,7 @@ public class HostPageData { public GerritConfig config; public Theme theme; public List plugins; + public List messages; public static class Theme { public String backgroundColor; @@ -39,4 +41,10 @@ public class HostPageData { public String tableOddRowColor; public String tableEvenRowColor; } + + public static class Message { + public String id; + public Date redisplay; + public String html; + } } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java new file mode 100644 index 0000000000..033e7b480f --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/MessageOfTheDay.java @@ -0,0 +1,69 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// 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 com.google.gerrit.extensions.systemstatus; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * Supplies a message of the day when the page is first loaded. + * + *
+ * DynamicSet.bind(binder(), MessageOfTheDay.class).to(MyMessage.class);
+ * 
+ */ +@ExtensionPoint +public abstract class MessageOfTheDay { + /** + * Retrieve the message of the day as an HTML fragment. + * + * @return message as an HTML fragment; null if no message is available. + */ + public abstract String getHtmlMessage(); + + /** + * Unique identifier for this message. + *

+ * Messages with the same identifier will be hidden from the user until + * redisplay has occurred. + *

+ * + * @return unique message identifier. This identifier should be unique within + * the server. + */ + public abstract String getMessageId(); + + /** + * When should the message be displayed? + * + *

+ * Default implementation returns {@code tomorrow at 00:00:00 GMT}. + *

+ * + * @return a future date after which the message should be redisplayed. + */ + public Date getRedisplay() { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + cal.add(Calendar.DAY_OF_MONTH, 1); + return cal.getTime(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java index 9a6db2ff44..94e1847873 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java @@ -565,6 +565,9 @@ public class Gerrit implements EntryPoint { } saveDefaultTheme(); + if (hpd.messages != null) { + new MessageOfTheDayBar(hpd.messages).show(); + } PluginLoader.load(hpd.plugins, token); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java new file mode 100644 index 0000000000..fa569b8684 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java @@ -0,0 +1,89 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// 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 com.google.gerrit.client; + +import com.google.gerrit.common.data.HostPageData; +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.Cookies; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwtexpui.safehtml.client.SafeHtml; +import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** Displays pending messages from the server. */ +class MessageOfTheDayBar extends Composite { + interface Binder extends UiBinder {} + private static final Binder uiBinder = GWT.create(Binder.class); + + private final List motd; + @UiField HTML message; + @UiField Anchor dismiss; + + MessageOfTheDayBar(List motd) { + this.motd = filter(motd); + initWidget(uiBinder.createAndBindUi(this)); + + SafeHtmlBuilder b = new SafeHtmlBuilder(); + if (motd.size() == 1) { + b.append(SafeHtml.asis(motd.get(0).html)); + } else { + for (HostPageData.Message m : motd) { + b.openDiv(); + b.append(SafeHtml.asis(m.html)); + b.closeDiv(); + } + } + message.setHTML(b); + } + + void show() { + if (!motd.isEmpty()) { + RootPanel.get().add(this); + } + } + + @UiHandler("dismiss") + void onDismiss(ClickEvent e) { + removeFromParent(); + + for (HostPageData.Message m : motd) { + Cookies.setCookie(cookieName(m), "1", m.redisplay); + } + } + + private static List filter(List in) { + List show = new ArrayList(); + for (HostPageData.Message m : in) { + if (Cookies.getCookie(cookieName(m)) == null) { + show.add(m); + } + } + return show; + } + + private static String cookieName(HostPageData.Message m) { + return "msg-" + m.id; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml new file mode 100644 index 0000000000..ff50ec6fc1 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml @@ -0,0 +1,71 @@ + + + + + .popup { + position: fixed; + top: 5px; + left: 50%; + margin-left: -200px; + z-index: 201; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 12px; + padding-right: 12px; + background: #FFF1A8; + border-radius: 10px; + } + + @if user.agent safari { + .popup { + \-webkit-border-radius: 10px; + } + } + @if user.agent gecko1_8 { + .popup { + \-moz-border-radius: 10px; + } + } + + .message { + display: inline; + } + .message a { + color: #222; + text-decoration: underline; + } + a.action { + color: #222; + text-decoration: underline; + display: inline-block; + margin-left: 0.5em; + } + + + + + + Dismiss + + + diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java index 508c41a3c0..20fd617bb3 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java @@ -14,6 +14,7 @@ package com.google.gerrit.httpd.raw; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; @@ -22,6 +23,7 @@ import com.google.gerrit.common.Version; import com.google.gerrit.common.data.GerritConfig; import com.google.gerrit.common.data.HostPageData; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.systemstatus.MessageOfTheDay; import com.google.gerrit.extensions.webui.WebUiPlugin; import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.httpd.WebSession; @@ -51,6 +53,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,6 +77,7 @@ public class HostPageServlet extends HttpServlet { private final Provider session; private final GerritConfig config; private final DynamicSet plugins; + private final DynamicSet messages; private final HostPageData.Theme signedOutTheme; private final HostPageData.Theme signedInTheme; private final SitePaths site; @@ -89,6 +93,7 @@ public class HostPageServlet extends HttpServlet { final SitePaths sp, final ThemeFactory themeFactory, final GerritConfig gc, final ServletContext servletContext, final DynamicSet webUiPlugins, + final DynamicSet motd, @GerritServerConfig final Config cfg, final StaticServlet ss) throws IOException, ServletException { @@ -96,6 +101,7 @@ public class HostPageServlet extends HttpServlet { session = w; config = gc; plugins = webUiPlugins; + messages = motd; signedOutTheme = themeFactory.getSignedOutTheme(); signedInTheme = themeFactory.getSignedInTheme(); site = sp; @@ -201,6 +207,7 @@ public class HostPageServlet extends HttpServlet { w.write(";"); } plugins(w); + messages(w); final byte[] hpd = w.toString().getBytes("UTF-8"); final byte[] raw = Bytes.concat(page.part1, hpd, page.part2); @@ -238,6 +245,25 @@ public class HostPageServlet extends HttpServlet { } } + private void messages(StringWriter w) { + List list = new ArrayList<>(2); + for (MessageOfTheDay motd : messages) { + String html = motd.getHtmlMessage(); + if (!Strings.isNullOrEmpty(html)) { + HostPageData.Message m = new HostPageData.Message(); + m.id = motd.getMessageId(); + m.redisplay = motd.getRedisplay(); + m.html = html; + list.add(m); + } + } + if (!list.isEmpty()) { + w.write(HPD_ID + ".messages="); + json(list, w); + w.write(";"); + } + } + private Page.Content select(HttpServletRequest req) { Page pg = get(); if ("1".equals(req.getParameter("dbg"))) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index d9bf706604..c5327b4d1f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -30,6 +30,7 @@ import com.google.gerrit.extensions.events.ProjectDeletedListener; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.systemstatus.MessageOfTheDay; import com.google.gerrit.extensions.webui.TopMenu; import com.google.gerrit.rules.PrologModule; import com.google.gerrit.rules.RulesCache; @@ -260,6 +261,7 @@ public class GerritGlobalModule extends FactoryModule { DynamicItem.itemOf(binder(), AvatarProvider.class); DynamicSet.setOf(binder(), LifecycleListener.class); DynamicSet.setOf(binder(), TopMenu.class); + DynamicSet.setOf(binder(), MessageOfTheDay.class); DynamicMap.mapOf(binder(), DownloadScheme.class); DynamicMap.mapOf(binder(), DownloadCommand.class);