Allow defining a default dashboard for projects
It is now possible to define a default dashboard for a project in the project.config file in the refs/meta/config branch. Example: [dashboard] default = refs/meta/dashboards/main:default When it will be possible to inherit dashboards from parent projects, it will be important to differentiate between a default dashboard that is be used for this project and the dashboard that will be inherited by the child projects as default dashboard. This is why we allow overwriting the default dashboard by explicitly setting a local default dashboard, which will only be used by this project. Example: [dashboard] default = refs/meta/dashboards/main:default local-default = refs/meta/dashboards/main:local Via the /dashboards/ REST endpoint it is now possible to either retrieve all dashboards of the project or only the default dashboard of the project. Change-Id: I119408968bc86c3fd112c186638d3176235ca28e Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
		@@ -420,7 +420,32 @@ List all dashboards for the `myProject` project:
 | 
			
		||||
      "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"
 | 
			
		||||
      "parameters": "title\u003dMyDashboard\u0026Open+Changes\u003dstatus:open project:myProject limit:15\u0026Merged+Changes\u003dstatus:merged project:myProject limit:15",
 | 
			
		||||
      "is_default": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
To retrieve only the default dashboard of a project set the parameter `default`.
 | 
			
		||||
 | 
			
		||||
----
 | 
			
		||||
  GET /dashboards/project/myProject?default&format=JSON&d HTTP/1.0
 | 
			
		||||
 | 
			
		||||
  HTTP/1.1 200 OK
 | 
			
		||||
  Content-Disposition: attachment
 | 
			
		||||
  Content-Type: application/json;charset=UTF-8
 | 
			
		||||
 | 
			
		||||
  )]}'
 | 
			
		||||
  {
 | 
			
		||||
    "MyProject Dashboard": {
 | 
			
		||||
      "kind": "gerritcodereview#dashboard",
 | 
			
		||||
      "id" : "refs/meta/dashboards/main:MyDashboard",
 | 
			
		||||
      "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",
 | 
			
		||||
      "is_default": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
----
 | 
			
		||||
 
 | 
			
		||||
@@ -77,6 +77,29 @@ section.<name>.query::
 | 
			
		||||
The change query that should be used to populate the section with the
 | 
			
		||||
given name.
 | 
			
		||||
 | 
			
		||||
Project Default Dashboard
 | 
			
		||||
-------------------------
 | 
			
		||||
 | 
			
		||||
It is possible to define a default dashboard for a project in the
 | 
			
		||||
projects `project.config` file in the `refs/meta/config` branch:
 | 
			
		||||
 | 
			
		||||
----
 | 
			
		||||
[dashboard]
 | 
			
		||||
  default = refs/meta/dashboards/main:default
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
The dashboard set as the default dashboard will be inherited as the
 | 
			
		||||
default dashboard by child projects if they do not define their own
 | 
			
		||||
default dashboard. The `local-default` entry makes it possible to
 | 
			
		||||
define a different default dashboard that is only used by this project
 | 
			
		||||
but not inherited to the child projects.
 | 
			
		||||
 | 
			
		||||
----
 | 
			
		||||
[dashboard]
 | 
			
		||||
  default = refs/meta/dashboards/main:default
 | 
			
		||||
  local-default = refs/meta/dashboards/main:local
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
GERRIT
 | 
			
		||||
------
 | 
			
		||||
Part of link:index.html[Gerrit Code Review]
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ public class DashboardInfo extends JavaScriptObject {
 | 
			
		||||
  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; }-*/;
 | 
			
		||||
  public final native boolean isDefault() /*-{ return this.isDefault ? true : false; }-*/;
 | 
			
		||||
 | 
			
		||||
  protected DashboardInfo() {
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,13 @@ public class DashboardMap extends NativeMap<DashboardInfo> {
 | 
			
		||||
        .send(NativeMap.copyKeysIntoChildren(callback));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static void projectDefault(Project.NameKey project, boolean mine,
 | 
			
		||||
      AsyncCallback<DashboardMap> callback) {
 | 
			
		||||
    new RestApi("/dashboards/project/" + URL.encode(project.get()).replaceAll("[?]", "%3F"))
 | 
			
		||||
        .addParameterTrue("default")
 | 
			
		||||
        .send(NativeMap.copyKeysIntoChildren(callback));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected DashboardMap() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -109,6 +109,10 @@ public final class Project {
 | 
			
		||||
 | 
			
		||||
  protected InheritedBoolean useContentMerge;
 | 
			
		||||
 | 
			
		||||
  protected String defaultDashboardId;
 | 
			
		||||
 | 
			
		||||
  protected String localDefaultDashboardId;
 | 
			
		||||
 | 
			
		||||
  protected Project() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -186,6 +190,22 @@ public final class Project {
 | 
			
		||||
    state = newState;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public String getDefaultDashboard() {
 | 
			
		||||
    return defaultDashboardId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void setDefaultDashboard(final String defaultDashboardId) {
 | 
			
		||||
    this.defaultDashboardId = defaultDashboardId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public String getLocalDefaultDashboard() {
 | 
			
		||||
    return localDefaultDashboardId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void setLocalDefaultDashboard(final String localDefaultDashboardId) {
 | 
			
		||||
    this.localDefaultDashboardId = localDefaultDashboardId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void copySettingsFrom(final Project update) {
 | 
			
		||||
    description = update.description;
 | 
			
		||||
    useContributorAgreements = update.useContributorAgreements;
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ 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.eclipse.jgit.treewalk.filter.PathFilter;
 | 
			
		||||
import org.kohsuke.args4j.Option;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
@@ -47,6 +48,7 @@ import java.io.PrintWriter;
 | 
			
		||||
import java.io.UnsupportedEncodingException;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.StringTokenizer;
 | 
			
		||||
 | 
			
		||||
/** List projects visible to the calling user. */
 | 
			
		||||
public class ListDashboards {
 | 
			
		||||
@@ -64,6 +66,9 @@ public class ListDashboards {
 | 
			
		||||
  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
 | 
			
		||||
  private OutputFormat format = OutputFormat.JSON;
 | 
			
		||||
 | 
			
		||||
  @Option(name = "--default", usage = "only the projects default dashboard is returned")
 | 
			
		||||
  private boolean defaultDashboard;
 | 
			
		||||
 | 
			
		||||
  private Level level;
 | 
			
		||||
  private String entityName;
 | 
			
		||||
 | 
			
		||||
@@ -111,7 +116,14 @@ public class ListDashboards {
 | 
			
		||||
      if (level != null) {
 | 
			
		||||
        switch (level) {
 | 
			
		||||
          case PROJECT:
 | 
			
		||||
            dashboards = projectDashboards(new Project.NameKey(entityName));
 | 
			
		||||
            final Project.NameKey projectName = new Project.NameKey(entityName);
 | 
			
		||||
            if (defaultDashboard) {
 | 
			
		||||
              dashboards = Maps.newTreeMap();
 | 
			
		||||
              final DashboardInfo info = loadProjectDefaultDashboard(projectName);
 | 
			
		||||
              dashboards.put(info.id, info);
 | 
			
		||||
            } else {
 | 
			
		||||
              dashboards = projectDashboards(projectName);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          default:
 | 
			
		||||
            throw new IllegalStateException("unsupported dashboard level: " + level);
 | 
			
		||||
@@ -145,7 +157,8 @@ public class ListDashboards {
 | 
			
		||||
      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));
 | 
			
		||||
          dashboards.putAll(loadDashboards(projectControl.getProject(), repo,
 | 
			
		||||
              revWalk, ref));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (IOException e) {
 | 
			
		||||
@@ -163,8 +176,8 @@ public class ListDashboards {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private Map<String, DashboardInfo> loadDashboards(
 | 
			
		||||
      final Project.NameKey projectName, final Repository repo,
 | 
			
		||||
      final RevWalk revWalk, final Ref ref) {
 | 
			
		||||
      final Project project, final Repository repo,
 | 
			
		||||
      final RevWalk revWalk, final Ref ref) throws IOException {
 | 
			
		||||
    final Map<String, DashboardInfo> dashboards = Maps.newTreeMap();
 | 
			
		||||
    TreeWalk treeWalk = new TreeWalk(repo);
 | 
			
		||||
    try {
 | 
			
		||||
@@ -173,39 +186,18 @@ public class ListDashboards {
 | 
			
		||||
      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();
 | 
			
		||||
 | 
			
		||||
        final DashboardInfo info =
 | 
			
		||||
            loadDashboard(project, ref.getName(), treeWalk.getPathString(),
 | 
			
		||||
                loader);
 | 
			
		||||
        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()
 | 
			
		||||
      log.warn("Failed to load dashboards of project " + project.getName()
 | 
			
		||||
          + " from ref " + ref.getName(), e);
 | 
			
		||||
    } catch (IOException e) {
 | 
			
		||||
      log.warn("Failed to load dashboards of project " + project.getName()
 | 
			
		||||
          + " from ref " + ref.getName(), e);
 | 
			
		||||
    } finally {
 | 
			
		||||
      treeWalk.release();
 | 
			
		||||
@@ -213,6 +205,110 @@ public class ListDashboards {
 | 
			
		||||
    return dashboards;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private DashboardInfo loadProjectDefaultDashboard(final Project.NameKey projectName) {
 | 
			
		||||
    final ProjectState projectState = projectCache.get(projectName);
 | 
			
		||||
    final ProjectControl projectControl = projectState.controlFor(currentUser);
 | 
			
		||||
    if (projectState == null || !projectControl.isVisible()) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final Project project = projectControl.getProject();
 | 
			
		||||
    final String defaultDashboardId =
 | 
			
		||||
        project.getLocalDefaultDashboard() != null ? project
 | 
			
		||||
            .getLocalDefaultDashboard() : project.getDefaultDashboard();
 | 
			
		||||
    return loadDashboard(projectControl, defaultDashboardId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private DashboardInfo loadDashboard(final ProjectControl projectControl,
 | 
			
		||||
      final String dashboardId) {
 | 
			
		||||
    StringTokenizer t = new StringTokenizer(dashboardId);
 | 
			
		||||
    if (t.countTokens() != 2) {
 | 
			
		||||
      throw new IllegalStateException("failed to load dashboard, invalid dashboard id: " + dashboardId);
 | 
			
		||||
    }
 | 
			
		||||
    final String refName = t.nextToken();
 | 
			
		||||
    final String path = t.nextToken();
 | 
			
		||||
 | 
			
		||||
    Repository repo = null;
 | 
			
		||||
    RevWalk revWalk = null;
 | 
			
		||||
    TreeWalk treeWalk = null;
 | 
			
		||||
    try {
 | 
			
		||||
      repo =
 | 
			
		||||
          repoManager.openRepository(projectControl.getProject().getNameKey());
 | 
			
		||||
      final Ref ref = repo.getRef(refName);
 | 
			
		||||
      if (ref == null) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!projectControl.controlForRef(ref.getName()).canRead()) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      revWalk = new RevWalk(repo);
 | 
			
		||||
      final RevCommit commit = revWalk.parseCommit(ref.getObjectId());
 | 
			
		||||
      treeWalk = new TreeWalk(repo);
 | 
			
		||||
      treeWalk.addTree(commit.getTree());
 | 
			
		||||
      treeWalk.setRecursive(true);
 | 
			
		||||
      treeWalk.setFilter(PathFilter.create(path));
 | 
			
		||||
      if (!treeWalk.next()) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      final ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
 | 
			
		||||
      return loadDashboard(projectControl.getProject(), refName, path, loader);
 | 
			
		||||
    } catch (IOException e) {
 | 
			
		||||
      log.warn("Failed to load default dashboard", e);
 | 
			
		||||
    } catch (ConfigInvalidException e) {
 | 
			
		||||
      log.warn("Failed to load dashboards of project "
 | 
			
		||||
          + projectControl.getProject().getName() + " from ref " + refName, e);
 | 
			
		||||
    } finally {
 | 
			
		||||
      if (treeWalk != null) {
 | 
			
		||||
        treeWalk.release();
 | 
			
		||||
      }
 | 
			
		||||
      if (revWalk != null) {
 | 
			
		||||
        revWalk.release();
 | 
			
		||||
      }
 | 
			
		||||
      if (repo != null) {
 | 
			
		||||
        repo.close();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private DashboardInfo loadDashboard(final Project project,
 | 
			
		||||
      final String refName, final String path, final ObjectLoader loader)
 | 
			
		||||
      throws IOException, ConfigInvalidException {
 | 
			
		||||
    DashboardInfo info = new DashboardInfo();
 | 
			
		||||
    info.dashboardName = path;
 | 
			
		||||
    info.refName = refName;
 | 
			
		||||
    info.projectName = project.getName();
 | 
			
		||||
    info.id = createId(info.refName, info.dashboardName);
 | 
			
		||||
    final String defaultDashboardId =
 | 
			
		||||
        project.getLocalDefaultDashboard() != null ? project
 | 
			
		||||
            .getLocalDefaultDashboard() : project.getDefaultDashboard();
 | 
			
		||||
    info.isDefault = info.id.equals(defaultDashboardId);
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    return info;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static String createId(final String refName,
 | 
			
		||||
      final String dashboardName) {
 | 
			
		||||
    return refName + ":" + dashboardName;
 | 
			
		||||
@@ -227,5 +323,6 @@ public class ListDashboards {
 | 
			
		||||
    String projectName;
 | 
			
		||||
    String description;
 | 
			
		||||
    String parameters;
 | 
			
		||||
    boolean isDefault;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -94,6 +94,10 @@ public class ProjectConfig extends VersionedMetaData {
 | 
			
		||||
  private static final String KEY_MERGE_CONTENT = "mergeContent";
 | 
			
		||||
  private static final String KEY_STATE = "state";
 | 
			
		||||
 | 
			
		||||
  private static final String DASHBOARD = "dashboard";
 | 
			
		||||
  private static final String KEY_DEFAULT = "default";
 | 
			
		||||
  private static final String KEY_LOCAL_DEFAULT = "local-default";
 | 
			
		||||
 | 
			
		||||
  private static final SubmitType defaultSubmitAction =
 | 
			
		||||
      SubmitType.MERGE_IF_NECESSARY;
 | 
			
		||||
  private static final State defaultStateValue =
 | 
			
		||||
@@ -291,6 +295,9 @@ public class ProjectConfig extends VersionedMetaData {
 | 
			
		||||
    p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, Project.InheritedBoolean.INHERIT));
 | 
			
		||||
    p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
 | 
			
		||||
 | 
			
		||||
    p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
 | 
			
		||||
    p.setLocalDefaultDashboard(rc.getString(DASHBOARD, null, KEY_LOCAL_DEFAULT));
 | 
			
		||||
 | 
			
		||||
    loadAccountsSection(rc, groupsByName);
 | 
			
		||||
    loadContributorAgreements(rc, groupsByName);
 | 
			
		||||
    loadAccessSections(rc, groupsByName);
 | 
			
		||||
@@ -538,6 +545,9 @@ public class ProjectConfig extends VersionedMetaData {
 | 
			
		||||
 | 
			
		||||
    set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
 | 
			
		||||
 | 
			
		||||
    set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
 | 
			
		||||
    set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
 | 
			
		||||
 | 
			
		||||
    Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
 | 
			
		||||
    saveAccountsSection(rc, keepGroups);
 | 
			
		||||
    saveContributorAgreements(rc, keepGroups);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user