Add top menu extension point

This extension point allows plugins to contribute entries to the top
menu of Gerrit's Web UI.

The usage is really simple, just implement TopMenu and add @Listen
annotation or bind the implementation in plugin's module using:

  DynamicSet.bind(binder(), TopMenu.class).to(PluginFooTopMenu.class);

Currently we only allow contribute top level menu entries with given
name and list of menu items with name and URL.

Change-Id: Ie161e0a61fdf041340df1aa0d6945fd479f34842
Signed-off-by: Dariusz Luksza <dariusz@luksza.org>
This commit is contained in:
Dariusz Luksza 2013-05-07 17:21:23 +02:00 committed by Edwin Kempin
parent 0b7e7850e5
commit 589ba00aa3
13 changed files with 393 additions and 1 deletions

View File

@ -732,6 +732,53 @@ Gerrit.install(function(self) {
});
----
[[top-menu-extensions]]
Top Menu Extensions
-------------------
Plugins can contribute items to Gerrit's top menu.
A single top menu extension can have multiple elements and will be put as
the last element in Gerrit's top menu.
Plugins define the top menu entries by implementing `TopMenu` interface:
[source,java]
----
public class MyTopMenuExtension implements TopMenu {
@Override
public List<MenuEntry> getEntries() {
return Lists.newArrayList(
new MenuEntry("Top Menu Entry", Lists.newArrayList(
new MenuItem("Gerrit", "http://gerrit.googlecode.com/"))));
}
}
----
If no Guice modules are declared in the manifest, the top menu extension may use
auto-registration by providing an `@Listen` annotation:
[source,java]
----
@Listen
public class MyTopMenuExtension implements TopMenu {
...
}
----
Otherwise the top menu extension must be bound in a plugin module:
[source,java]
----
public class HelloWorldModule extends AbstractModule {
@Override
protected void configure() {
DynamicSet.bind(binder(), TopMenu.class).to(MyTopMenuExtension.class);
}
}
----
[[http]]
HTTP Servlets
-------------

View File

@ -135,6 +135,47 @@ The entries in the map are sorted by capability ID.
}
----
[[get-top-menus]]
Get Top Menus
~~~~~~~~~~~~~
[verse]
'GET /config/server/top-menus'
Returns the list of additional top menu entries.
.Request
----
GET /config/server/top-menus HTTP/1.0
----
As response a list of the additional top menu entries as
link:#top-menu-entry-info[TopMenuEntryInfo] entities is returned.
.Response
----
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
)]}'
[
{
"name": "Top Menu Entry",
"items": [
{
"url": "http://gerrit.googlecode.com/",
"name": "Gerrit",
"target": "_blank"
}
]
}
]
----
[[json-entities]]
JSON Entities
-------------
[[capability-info]]
CapabilityInfo
~~~~~~~~~~~~~~
@ -148,6 +189,32 @@ The `CapabilityInfo` entity contains information about a capability.
|`name` |capability name
|=================================
[[top-menu-entry-info]]
TopMenuEntryInfo
~~~~~~~~~~~~~~~~
The `TopMenuEntryInfo` entity contains information about a top menu
entry.
[options="header",width="50%",cols="1,5"]
|=================================
|Field Name |Description
|`name` |Name of the top menu entry.
|`items` |List of link:#top-menu-item-info[menu items].
|=================================
[[top-menu-item-info]]
TopMenuItemInfo
~~~~~~~~~~~~~~~
The `TopMenuItemInfo` entity contains information about a menu item in
a top menu entry.
[options="header",width="50%",cols="1,5"]
|=================================
|Field Name |Description
|`url` |The URL of the menu item link.
|`name` |The name of the menu item.
|`target` |Target attribute of the menu item link.
|=================================
GERRIT
------

View File

@ -0,0 +1,51 @@
// Copyright (C) 2013 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.webui;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import java.util.List;
@ExtensionPoint
public interface TopMenu {
public class MenuEntry {
public final String name;
public final List<MenuItem> items;
public MenuEntry(String name, List<MenuItem> items) {
this.name = name;
this.items = items;
}
}
public class MenuItem {
public final String url;
public final String name;
public final String target;
public MenuItem(String name, String url) {
this(name, url, "_blank");
}
public MenuItem(String name, String url, String target) {
this.url = url;
this.name = name;
this.target = target;
}
}
List<MenuEntry> getEntries();
}

View File

@ -24,8 +24,13 @@ import com.google.gerrit.client.admin.ProjectScreen;
import com.google.gerrit.client.api.ApiGlue;
import com.google.gerrit.client.changes.ChangeConstants;
import com.google.gerrit.client.changes.ChangeListScreen;
import com.google.gerrit.client.config.ConfigServerApi;
import com.google.gerrit.client.extensions.TopMenu;
import com.google.gerrit.client.extensions.TopMenuItem;
import com.google.gerrit.client.extensions.TopMenuList;
import com.google.gerrit.client.patches.PatchScreen;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.LinkMenuBar;
import com.google.gerrit.client.ui.LinkMenuItem;
import com.google.gerrit.client.ui.MorphingTabPanel;
@ -86,6 +91,7 @@ import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.client.KeyUtil;
import java.util.ArrayList;
import java.util.List;
public class Gerrit implements EntryPoint {
public static final GerritConstants C = GWT.create(GerritConstants.class);
@ -784,6 +790,18 @@ public class Gerrit implements EntryPoint {
break;
}
}
ConfigServerApi.topMenus(new GerritCallback<TopMenuList>() {
public void onSuccess(TopMenuList result) {
List<TopMenu> topMenuExtensions = Natives.asList(result);
for (TopMenu menu : topMenuExtensions) {
LinkMenuBar bar = new LinkMenuBar();
for (TopMenuItem item : Natives.asList(menu.getItems())) {
addExtensionLink(bar, item);
}
menuLeft.add(bar, menu.getName());
}
};
});
}
public static void applyUserPreferences() {
@ -916,4 +934,10 @@ public class Gerrit implements EntryPoint {
atag.setTarget("_blank");
m.add(atag);
}
private static void addExtensionLink(final LinkMenuBar m, final TopMenuItem item) {
final Anchor atag = anchor(item.getName(), item.getUrl());
atag.setTarget(item.getTarget());
m.add(atag);
}
}

View File

@ -14,6 +14,7 @@
package com.google.gerrit.client.config;
import com.google.gerrit.client.extensions.TopMenuList;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gwt.user.client.rpc.AsyncCallback;
@ -27,4 +28,8 @@ public class ConfigServerApi {
public static void capabilities(AsyncCallback<NativeMap<CapabilityInfo>> cb) {
new RestApi("/config/server/capabilities/").get(cb);
}
public static void topMenus(AsyncCallback<TopMenuList> cb) {
new RestApi("/config/server/top-menus").get(cb);
}
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2013 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.extensions;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
public class TopMenu extends JavaScriptObject {
protected TopMenu() {
}
public final native String getName() /*-{ return this.name; }-*/;
public final native JsArray<TopMenuItem> getItems() /*-{ return this.items; }-*/;
}

View File

@ -0,0 +1,26 @@
// Copyright (C) 2013 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.extensions;
import com.google.gwt.core.client.JavaScriptObject;
public class TopMenuItem extends JavaScriptObject {
public final native String getName() /*-{ return this.name; }-*/;
public final native String getUrl() /*-{ return this.url; }-*/;
public final native String getTarget() /*-{ return this.target; }-*/;
protected TopMenuItem() {
}
}

View File

@ -0,0 +1,23 @@
// Copyright (C) 2013 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.extensions;
import com.google.gwt.core.client.JsArray;
public class TopMenuList extends JsArray<TopMenu> {
protected TopMenuList() {
}
}

View File

@ -27,6 +27,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.webui.TopMenu;
import com.google.gerrit.rules.PrologModule;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.AnonymousUser;
@ -239,6 +240,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), MergeValidationListener.class);
DynamicItem.itemOf(binder(), AvatarProvider.class);
DynamicSet.setOf(binder(), LifecycleListener.class);
DynamicSet.setOf(binder(), TopMenu.class);
bind(AnonymousUser.class);

View File

@ -0,0 +1,41 @@
// Copyright (C) 2013 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.server.config;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.webui.TopMenu;
import com.google.inject.Inject;
import java.util.List;
class ListTopMenus implements RestReadView<ConfigResource> {
private final DynamicSet<TopMenu> extensions;
@Inject
ListTopMenus(DynamicSet<TopMenu> extensions) {
this.extensions = extensions;
}
@Override
public Object apply(ConfigResource resource) {
List<TopMenu.MenuEntry> entries = Lists.newArrayList();
for (TopMenu extension : extensions) {
entries.addAll(extension.getEntries());
}
return entries;
}
}

View File

@ -14,8 +14,9 @@
package com.google.gerrit.server.config;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
import static com.google.gerrit.server.config.CapabilityResource.CAPABILITY_KIND;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
import static com.google.gerrit.server.config.TopMenuResource.TOP_MENU_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
@ -24,8 +25,10 @@ public class Module extends RestApiModule {
@Override
protected void configure() {
DynamicMap.mapOf(binder(), CONFIG_KIND);
DynamicMap.mapOf(binder(), TOP_MENU_KIND);
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
get(CONFIG_KIND, "version").to(GetVersion.class);
}
}

View File

@ -0,0 +1,52 @@
// Copyright (C) 2013 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.server.config;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.inject.Inject;
import com.google.inject.Provider;
class TopMenuCollection implements
ChildCollection<ConfigResource, TopMenuResource> {
private final DynamicMap<RestView<TopMenuResource>> views;
private final Provider<ListTopMenus> list;
@Inject
TopMenuCollection(DynamicMap<RestView<TopMenuResource>> views,
Provider<ListTopMenus> list) {
this.views = views;
this.list = list;
}
@Override
public RestView<ConfigResource> list() throws ResourceNotFoundException {
return list.get();
}
@Override
public TopMenuResource parse(ConfigResource parent, IdString id)
throws ResourceNotFoundException {
throw new ResourceNotFoundException(id);
}
@Override
public DynamicMap<RestView<TopMenuResource>> views() {
return views;
}
}

View File

@ -0,0 +1,23 @@
// Copyright (C) 2013 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.server.config;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.inject.TypeLiteral;
public class TopMenuResource extends ConfigResource {
public static final TypeLiteral<RestView<TopMenuResource>> TOP_MENU_KIND =
new TypeLiteral<RestView<TopMenuResource>>() {};
}