Enable storing of custom dashboards for projects

Custom dashboards can now be stored in the projects
`refs/meta/dashboards/*` branches. A REST endpoint was added to
retrieve the custom dashboards for a project.

Change-Id: I1be4c4b8856f4edd279e752d5b4004f9a548bd2a
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2012-11-12 23:03:36 +01:00
parent f948259eb7
commit b2efe218a5
8 changed files with 435 additions and 0 deletions

View File

@@ -396,6 +396,35 @@ default. Optional fields are:
]
----
[[dashboards]]
/dashboards/project/ (List Dashboards)
~~~~~~~~~~~~~~~~~~~~~~~~~~
Lists custom dashboards for a project.
The `/dashboards/project/` URL expects the project name as part of the
URL.
List all dashboards for the `myProject` project:
----
GET /dashboards/project/myProject?format=JSON&d HTTP/1.0
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"refs/meta/dashboards/main:MyDashboard": {
"kind": "gerritcodereview#dashboard",
"id" : "refs/meta/dashboards/main:MyDashboard",
"dashboard_name": "MyDashboard",
"ref_name": "refs/meta/dashboards/main",
"project_name": "myProject",
"description": "Most recent open and merged changes.",
"parameters": "title\u003dMyDashboard\u0026Open+Changes\u003dstatus:open project:myProject limit:15\u0026Merged+Changes\u003dstatus:merged project:myProject limit:15"
}
}
----
GERRIT
------

View File

@@ -43,6 +43,40 @@ Parameters may be separated from each other using any of the following
characters, as some users may find one more readable than another:
`&` or `;` or `,`
Project Dashboards
------------------
It is possible to share custom dashboards at a project level. To do
this define the dashboards in a `refs/meta/dashboards/*` branch of the
project. For each dashboard create a config file. The file name will be
used as name for the dashboard.
Example dashboard config file `MyProject Dashboard`:
----
[main]
description = Most recent open and merged changes.
[section "Open Changes"]
query = status:open project:myProject limit:15
[section "Merged Changes"]
query = status:merged project:myProject limit:15
----
Section main
~~~~~~~~~~~~
main.description::
+
The description of the dashboard.
Section section
~~~~~~~~~~~~~~~
section.<name>.query::
+
The change query that should be used to populate the section with the
given name.
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@@ -0,0 +1,29 @@
// Copyright (C) 2012 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.dashboards;
import com.google.gwt.core.client.JavaScriptObject;
public class DashboardInfo extends JavaScriptObject {
public final native String id() /*-{ return this.id; }-*/;
public final native String name() /*-{ return this.dashboard_name; }-*/;
public final native String refName() /*-{ return this.ref_name; }-*/;
public final native String projectName() /*-{ return this.project_name; }-*/;
public final native String description() /*-{ return this.description; }-*/;
public final native String parameters() /*-{ return this.parameters; }-*/;
protected DashboardInfo() {
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (C) 2012 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.dashboards;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwt.http.client.URL;
/** Dashboards available from {@code /dashboards/}. */
public class DashboardMap extends NativeMap<DashboardInfo> {
public static void allOnProject(Project.NameKey project,
AsyncCallback<DashboardMap> callback) {
new RestApi("/dashboards/project/" + URL.encode(project.get()).replaceAll("[?]", "%3F"))
.send(NativeMap.copyKeysIntoChildren(callback));
}
protected DashboardMap() {
}
}

View File

@@ -27,6 +27,7 @@ import com.google.gerrit.httpd.raw.ToolServlet;
import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
import com.google.gerrit.httpd.rpc.dashboard.ListDashboardsServlet;
import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@@ -97,6 +98,7 @@ class UrlModule extends ServletModule {
serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
serveRegex("^/(?:a/)?dashboards/(.*)?$").with(ListDashboardsServlet.class);
if (cfg.deprecatedQuery) {
serve("/query").with(DeprecatedChangeQueryServlet.class);

View File

@@ -0,0 +1,75 @@
// Copyright (C) 2012 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.httpd.rpc.dashboard;
import com.google.common.base.Strings;
import com.google.gerrit.httpd.RestApiServlet;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.dashboard.ListDashboards;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class ListDashboardsServlet extends RestApiServlet {
private static final long serialVersionUID = 1L;
private static final String PROJECT_LEVEL_PREFIX = "project/";
private final ParameterParser paramParser;
private final Provider<ListDashboards> factory;
@Inject
ListDashboardsServlet(final Provider<CurrentUser> currentUser,
ParameterParser paramParser, Provider<ListDashboards> ls) {
super(currentUser);
this.paramParser = paramParser;
this.factory = ls;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
ListDashboards impl = factory.get();
if (!Strings.isNullOrEmpty(req.getPathInfo())) {
final String path = URLDecoder.decode(req.getPathInfo(), "UTF-8");
if (path.startsWith(PROJECT_LEVEL_PREFIX)) {
impl.setLevel(ListDashboards.Level.PROJECT);
impl.setEntityName(path.substring(PROJECT_LEVEL_PREFIX.length()));
}
}
if (acceptsJson(req)) {
impl.setFormat(OutputFormat.JSON_COMPACT);
}
if (paramParser.parse(impl, req, res)) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
if (impl.getFormat().isJson()) {
res.setContentType(JSON_TYPE);
buf.write(JSON_MAGIC);
} else {
res.setContentType("text/plain");
}
impl.display(buf);
res.setCharacterEncoding("UTF-8");
send(req, res, buf.toByteArray());
}
}
}

View File

@@ -31,6 +31,7 @@ import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
import com.google.gerrit.server.changedetail.PublishDraft;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.changedetail.Submit;
import com.google.gerrit.server.dashboard.ListDashboards;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.BanCommit;
import com.google.gerrit.server.git.CreateCodeReviewNotes;
@@ -73,6 +74,7 @@ public class GerritRequestModule extends FactoryModule {
bind(AccountResolver.class);
bind(ChangeQueryRewriter.class);
bind(ListProjects.class);
bind(ListDashboards.class);
bind(ApprovalsUtil.class);
bind(PerRequestProjectControlCache.class).in(RequestScoped.class);

View File

@@ -0,0 +1,231 @@
// Copyright (C) 2012 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.dashboard;
import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Set;
/** List projects visible to the calling user. */
public class ListDashboards {
private static final Logger log = LoggerFactory.getLogger(ListDashboards.class);
private static String REFS_DASHBOARDS = "refs/meta/dashboards/";
public static enum Level {
PROJECT
};
private final CurrentUser currentUser;
private final ProjectCache projectCache;
private final GitRepositoryManager repoManager;
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
private OutputFormat format = OutputFormat.JSON;
private Level level;
private String entityName;
@Inject
protected ListDashboards(CurrentUser currentUser, ProjectCache projectCache,
GitRepositoryManager repoManager) {
this.currentUser = currentUser;
this.projectCache = projectCache;
this.repoManager = repoManager;
}
public OutputFormat getFormat() {
return format;
}
public ListDashboards setFormat(OutputFormat fmt) {
if (!format.isJson()) {
throw new IllegalArgumentException(format.name() + " not supported");
}
this.format = fmt;
return this;
}
public ListDashboards setLevel(Level level) {
this.level = level;
return this;
}
public ListDashboards setEntityName(String entityName) {
this.entityName = entityName;
return this;
}
public void display(OutputStream out) {
final PrintWriter stdout;
try {
stdout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, "UTF-8")));
} catch (UnsupportedEncodingException e) {
// Our encoding is required by the specifications for the runtime.
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
}
try {
final Map<String, DashboardInfo> dashboards;
if (level != null) {
switch (level) {
case PROJECT:
dashboards = projectDashboards(new Project.NameKey(entityName));
break;
default:
throw new IllegalStateException("unsupported dashboard level: " + level);
}
} else {
dashboards = Maps.newTreeMap();
}
format.newGson().toJson(dashboards,
new TypeToken<Map<String, DashboardInfo>>() {}.getType(), stdout);
stdout.print('\n');
} finally {
stdout.flush();
}
}
private Map<String, DashboardInfo> projectDashboards(final Project.NameKey projectName) {
final Map<String, DashboardInfo> dashboards = Maps.newTreeMap();
final ProjectState projectState = projectCache.get(projectName);
final ProjectControl projectControl = projectState.controlFor(currentUser);
if (projectState == null || !projectControl.isVisible()) {
return dashboards;
}
Repository repo = null;
RevWalk revWalk = null;
try {
repo = repoManager.openRepository(projectName);
revWalk = new RevWalk(repo);
final Map<String, Ref> refs = repo.getRefDatabase().getRefs(REFS_DASHBOARDS);
for (final Ref ref : refs.values()) {
if (projectControl.controlForRef(ref.getName()).canRead()) {
dashboards.putAll(loadDashboards(projectName, repo, revWalk, ref));
}
}
} catch (IOException e) {
log.warn("Failed to load dashboards of project " + projectName.get(), e);
} finally {
if (revWalk != null) {
revWalk.release();
}
if (repo != null) {
repo.close();
}
}
return dashboards;
}
private Map<String, DashboardInfo> loadDashboards(
final Project.NameKey projectName, final Repository repo,
final RevWalk revWalk, final Ref ref) {
final Map<String, DashboardInfo> dashboards = Maps.newTreeMap();
TreeWalk treeWalk = new TreeWalk(repo);
try {
final RevCommit commit = revWalk.parseCommit(ref.getObjectId());
final RevTree tree = commit.getTree();
treeWalk.addTree(tree);
treeWalk.setRecursive(true);
while (treeWalk.next()) {
DashboardInfo info = new DashboardInfo();
info.dashboardName = treeWalk.getPathString();
info.refName = ref.getName();
info.projectName = projectName.get();
info.id = createId(info.refName, info.dashboardName);
final ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
ByteArrayOutputStream out = new ByteArrayOutputStream();
loader.copyTo(out);
Config dashboardConfig = new Config();
dashboardConfig.fromText(new String(out.toByteArray(), "UTF-8"));
info.description = dashboardConfig.getString("main", null, "description");
final StringBuilder query = new StringBuilder();
query.append("title=");
query.append(info.dashboardName.replaceAll(" ", "+"));
final Set<String> sections = dashboardConfig.getSubsections("section");
for (final String section : sections) {
query.append("&");
query.append(section.replaceAll(" ", "+"));
query.append("=");
query.append(dashboardConfig.getString("section", section, "query"));
}
info.parameters = query.toString();
dashboards.put(info.id, info);
}
} catch (IOException e) {
log.warn("Failed to load dashboards of project " + projectName.get()
+ " from ref " + ref.getName(), e);
} catch (ConfigInvalidException e) {
log.warn("Failed to load dashboards of project " + projectName.get()
+ " from ref " + ref.getName(), e);
} finally {
treeWalk.release();
}
return dashboards;
}
private static String createId(final String refName,
final String dashboardName) {
return refName + ":" + dashboardName;
}
@SuppressWarnings("unused")
private static class DashboardInfo {
final String kind = "gerritcodereview#dashboard";
String id;
String dashboardName;
String refName;
String projectName;
String description;
String parameters;
}
}