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
This commit is contained in:
Shawn Pearce 2014-01-02 08:01:52 -08:00
parent 17bb675699
commit dfbe6d6ead
7 changed files with 268 additions and 0 deletions

View File

@ -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<String> plugins;
public List<Message> 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;
}
}

View File

@ -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.
*
* <pre>
* DynamicSet.bind(binder(), MessageOfTheDay.class).to(MyMessage.class);
* </pre>
*/
@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.
* <p>
* Messages with the same identifier will be hidden from the user until
* redisplay has occurred.
* </p>
*
* @return unique message identifier. This identifier should be unique within
* the server.
*/
public abstract String getMessageId();
/**
* When should the message be displayed?
*
* <p>
* Default implementation returns {@code tomorrow at 00:00:00 GMT}.
* </p>
*
* @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();
}
}

View File

@ -565,6 +565,9 @@ public class Gerrit implements EntryPoint {
}
saveDefaultTheme();
if (hpd.messages != null) {
new MessageOfTheDayBar(hpd.messages).show();
}
PluginLoader.load(hpd.plugins, token);
}

View File

@ -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<HTMLPanel, MessageOfTheDayBar> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private final List<HostPageData.Message> motd;
@UiField HTML message;
@UiField Anchor dismiss;
MessageOfTheDayBar(List<HostPageData.Message> 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<HostPageData.Message> filter(List<HostPageData.Message> in) {
List<HostPageData.Message> show = new ArrayList<HostPageData.Message>();
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;
}
}

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<ui:UiBinder
xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style>
.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;
}
</ui:style>
<g:HTMLPanel styleName='{style.popup}'>
<g:HTML ui:field='message' styleName='{style.message}'/>
<g:Anchor ui:field='dismiss'
styleName='{style.action}'
href='javascript:;'
title='Hide this message'>
<ui:attribute name='title'/>
Dismiss
</g:Anchor>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -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<WebSession> session;
private final GerritConfig config;
private final DynamicSet<WebUiPlugin> plugins;
private final DynamicSet<MessageOfTheDay> 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<WebUiPlugin> webUiPlugins,
final DynamicSet<MessageOfTheDay> 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<HostPageData.Message> 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"))) {

View File

@ -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);