Add dashboards page for projects

This change links out to the dashboards via query parameters
A follow-up change will adjust the links to go to the project pages
after change 143970 is merged.

Bug: Issue 8110
Change-Id: Ifdd190557483c0cd067f7a89e81d77a32bbc180a
This commit is contained in:
Becky Siegel
2018-01-16 17:43:10 -08:00
parent d5a4f7d7c0
commit c588ab7607
12 changed files with 522 additions and 50 deletions

View File

@@ -32,6 +32,7 @@ limitations under the License.
<link rel="import" href="../gr-repo/gr-repo.html">
<link rel="import" href="../gr-repo-access/gr-repo-access.html">
<link rel="import" href="../gr-repo-commands/gr-repo-commands.html">
<link rel="import" href="../gr-repo-dashboards/gr-repo-dashboards.html">
<link rel="import" href="../gr-repo-detail-list/gr-repo-detail-list.html">
<link rel="import" href="../gr-repo-list/gr-repo-list.html">
@@ -132,6 +133,11 @@ limitations under the License.
repo="[[params.repo]]"></gr-repo-access>
</main>
</template>
<template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
<main class="table">
<gr-repo-dashboards repo="[[params.repo]]"></gr-repo-dashboards>
</main>
</template>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-admin-view.js"></script>

View File

@@ -75,6 +75,7 @@
_showGroupMembers: Boolean,
_showRepoAccess: Boolean,
_showRepoCommands: Boolean,
_showRepoDashboards: Boolean,
_showRepoDetailList: Boolean,
_showRepoMain: Boolean,
_showRepoList: Boolean,
@@ -142,6 +143,12 @@
view: Gerrit.Nav.View.REPO,
detailType: Gerrit.Nav.RepoDetailView.TAGS,
url: Gerrit.Nav.getUrlForRepoTags(this._repoName),
},
{
name: 'Dashboards',
view: Gerrit.Nav.View.REPO,
detailType: Gerrit.Nav.RepoDetailView.DASHBOARDS,
url: Gerrit.Nav.getUrlForRepoDashboards(this._repoName),
}],
};
}
@@ -206,6 +213,8 @@
this.set('_showRepoDetailList', isRepoView &&
(params.detail === Gerrit.Nav.RepoDetailView.BRANCHES ||
params.detail === Gerrit.Nav.RepoDetailView.TAGS));
this.set('_showRepoDashboards', isRepoView &&
params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS);
this.set('_showRepoMain', isRepoView && !params.detail);
this.set('_showRepoList', isAdminView &&

View File

@@ -242,50 +242,59 @@ limitations under the License.
});
});
test('repo list', done => {
test('repo list', () => {
element.params = {
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-repo-list',
openCreateModal: false,
};
flush(() => {
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Repositories');
done();
});
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Repositories');
});
test('repo', done => {
test('repo', () => {
element.params = {
view: Gerrit.Nav.View.REPO,
repoName: 'foo',
};
element._repoName = 'foo';
element.reload().then(() => {
flush(() => {
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'foo');
done();
});
return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'foo');
});
});
test('repo access', done => {
test('repo access', () => {
element.params = {
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.ACCESS,
repoName: 'foo',
};
element._repoName = 'foo';
element.reload().then(() => {
flush(() => {
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Access');
done();
});
return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Access');
});
});
test('repo dashboards', () => {
element.params = {
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
repoName: 'foo',
};
element._repoName = 'foo';
return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Dashboards');
});
});
});
@@ -307,50 +316,44 @@ limitations under the License.
.returns(Promise.resolve(true));
});
test('group list', done => {
test('group list', () => {
element.params = {
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-admin-group-list',
openCreateModal: false,
};
flush(() => {
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Groups');
done();
});
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Groups');
});
test('group', done => {
test('group', () => {
element.params = {
view: Gerrit.Nav.View.GROUP,
groupId: 1234,
};
element._groupName = 'foo';
element.reload().then(() => {
flush(() => {
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'foo');
done();
});
return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'foo');
});
});
test('group members', done => {
test('group members', () => {
element.params = {
view: Gerrit.Nav.View.GROUP,
detail: Gerrit.Nav.GroupDetailView.MEMBERS,
groupId: 1234,
};
element._groupName = 'foo';
element.reload().then(() => {
flush(() => {
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Members');
done();
});
return element.reload().then(() => {
flushAsynchronousOperations();
const selected = element.$$('gr-page-nav .selected');
assert.isOk(selected);
assert.equal(selected.textContent.trim(), 'Members');
});
});
});

View File

@@ -0,0 +1,67 @@
<!--
Copyright (C) 2018 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.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-repo-dashboards">
<template>
<style include="shared-styles">
:host {
display: block;
margin-bottom: 2em;
}
.loading #dashboards,
#loadingContainer {
display: none;
}
.loading #loadingContainer {
display: block;
}
</style>
<style include="gr-table-styles"></style>
<table id="list" class$="genericList [[_computeLoadingClass(_loading)]]">
<tr class="headerRow">
<th class="topHeader">Dashboard name</th>
<th class="topHeader">Dashboard title</th>
<th class="topHeader">Dashboard description</th>
<th class="topHeader">Inherited from</th>
<th class="topHeader">Default</th>
</tr>
<tr id="loadingContainer">
<td>Loading...</td>
</tr>
<tbody id="dashboards">
<template is="dom-repeat" items="[[_dashboards]]">
<tr class="groupHeader">
<td colspan="5">[[item.section]]</td>
</tr>
<template is="dom-repeat" items="[[item.dashboards]]">
<tr class="table">
<td class="name"><a href$="[[item.url]]">[[item.path]]</a></td>
<td class="title">[[item.title]]</td>
<td class="desc">[[item.description]]</td>
<td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td>
<td class="default">[[_computeIsDefault(item.is_default)]]</td>
</tr>
</template>
</template>
</tbody>
</table>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-repo-dashboards.js"></script>
</dom-module>

View File

@@ -0,0 +1,73 @@
// Copyright (C) 2017 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.
(function() {
'use strict';
Polymer({
is: 'gr-repo-dashboards',
properties: {
repo: {
type: String,
observer: '_repoChanged',
},
_loading: {
type: Boolean,
value: true,
},
_dashboards: Array,
},
_repoChanged(repo) {
this._loading = true;
if (!repo) { return Promise.resolve(); }
this.$.restAPI.getRepoDashboards(this.repo).then(res => {
// Flatten 2 dimenional array, and sort by id.
const dashboards = res.concat.apply([], res).sort((a, b) =>
a.id > b.id);
const customList = dashboards.filter(a => a.ref === 'custom');
const defaultList = dashboards.filter(a => a.ref === 'default');
const dashboardBuilder = [];
if (customList.length) {
dashboardBuilder.push({
section: 'Custom',
dashboards: customList,
});
}
if (defaultList.length) {
dashboardBuilder.push({
section: 'Default',
dashboards: defaultList,
});
}
this._dashboards = dashboardBuilder;
this._loading = false;
Polymer.dom.flush();
});
},
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
},
_computeInheritedFrom(project, definingProject) {
return project === definingProject ? '' : definingProject;
},
_computeIsDefault(isDefault) {
return isDefault ? '✓' : '';
},
});
})();

View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<!--
Copyright (C) 2017 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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-repo-dashboards</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-repo-dashboards.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-repo-dashboards></gr-repo-dashboards>
</template>
</test-fixture>
<script>
suite('gr-repo-dashboards tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
suite('with default only', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
Promise.resolve([
[
{
id: 'default:contributor',
project: 'gerrit',
defining_project: 'gerrit',
ref: 'default',
path: 'contributor',
description: 'Own contributions.',
foreach: 'owner:self',
url: '/dashboard/?params',
title: 'Contributor Dashboard',
sections: [
{
name: 'Mine To Rebase',
query: 'is:open -is:mergeable',
},
{
name: 'My Recently Merged',
query: 'is:merged limit:10',
},
],
},
],
[
{
id: 'default:open',
project: 'gerrit',
defining_project: 'Public-Projects',
ref: 'default',
path: 'open',
description: 'Recent open changes.',
url: '/dashboard/?params',
title: 'Open Changes',
sections: [
{
name: 'Open Changes',
query: 'status:open project:${project} -age:7w',
},
],
},
],
]));
});
test('loading', done => {
assert.isTrue(element._loading);
assert.notEqual(getComputedStyle(element.$.loadingContainer).display,
'none');
assert.equal(getComputedStyle(element.$.dashboards).display,
'none');
element.repo = 'test';
flush(() => {
assert.equal(element._dashboards.length, 1);
assert.equal(element._dashboards[0].section, 'Default');
assert.equal(element._dashboards[0].dashboards.length, 2);
assert.equal(getComputedStyle(element.$.loadingContainer).display,
'none');
assert.notEqual(getComputedStyle(element.$.dashboards).display,
'none');
done();
});
});
test('dispatched command-tap on button tap', done => {
element.repo = 'test';
flush(() => {
assert.equal(element._dashboards.length, 1);
assert.equal(element._dashboards[0].section, 'Default');
assert.equal(element._dashboards[0].dashboards.length, 2);
done();
});
});
});
suite('with custom only', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
Promise.resolve([
[
{
id: 'custom:custom1',
project: 'gerrit',
defining_project: 'gerrit',
ref: 'custom',
path: 'contributor',
description: 'Own contributions.',
foreach: 'owner:self',
url: '/dashboard/?params',
title: 'Contributor Dashboard',
sections: [
{
name: 'Mine To Rebase',
query: 'is:open -is:mergeable',
},
{
name: 'My Recently Merged',
query: 'is:merged limit:10',
},
],
},
],
[
{
id: 'custom:custom2',
project: 'gerrit',
defining_project: 'Public-Projects',
ref: 'custom',
path: 'open',
description: 'Recent open changes.',
url: '/dashboard/?params',
title: 'Open Changes',
sections: [
{
name: 'Open Changes',
query: 'status:open project:${project} -age:7w',
},
],
},
],
]));
});
test('dispatched command-tap on button tap', done => {
element.repo = 'test';
flush(() => {
assert.equal(element._dashboards.length, 1);
assert.equal(element._dashboards[0].section, 'Custom');
assert.equal(element._dashboards[0].dashboards.length, 2);
done();
});
});
});
suite('with custom and default', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns(
Promise.resolve([
[
{
id: 'default:contributor',
project: 'gerrit',
defining_project: 'gerrit',
ref: 'default',
path: 'contributor',
description: 'Own contributions.',
foreach: 'owner:self',
url: '/dashboard/?params',
title: 'Contributor Dashboard',
sections: [
{
name: 'Mine To Rebase',
query: 'is:open -is:mergeable',
},
{
name: 'My Recently Merged',
query: 'is:merged limit:10',
},
],
},
],
[
{
id: 'custom:custom2',
project: 'gerrit',
defining_project: 'Public-Projects',
ref: 'custom',
path: 'open',
description: 'Recent open changes.',
url: '/dashboard/?params',
title: 'Open Changes',
sections: [
{
name: 'Open Changes',
query: 'status:open project:${project} -age:7w',
},
],
},
],
]));
});
test('dispatched command-tap on button tap', done => {
element.repo = 'test';
flush(() => {
assert.equal(element._dashboards.length, 2);
assert.equal(element._dashboards[0].section, 'Custom');
assert.equal(element._dashboards[1].section, 'Default');
assert.equal(element._dashboards[0].dashboards.length, 1);
assert.equal(element._dashboards[1].dashboards.length, 1);
done();
});
});
});
});
</script>

View File

@@ -99,6 +99,7 @@ limitations under the License.
ACCESS: 'access',
BRANCHES: 'branches',
COMMANDS: 'commands',
DASHBOARDS: 'dashboards',
TAGS: 'tags',
},
@@ -436,6 +437,18 @@ limitations under the License.
});
},
/**
* @param {string} repoName
* @return {string}
*/
getUrlForRepoDashboards(repoName) {
return this._getUrlFor({
view: Gerrit.Nav.View.REPO,
repoName,
detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
});
},
/**
* @param {string} groupId
* @return {string}

View File

@@ -67,6 +67,9 @@
// Matches /admin/repos/<repos>,access.
REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
// Matches /admin/repos/<repos>,access.
REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
// Matches /admin/repos[,<offset>][/].
REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
@@ -460,6 +463,8 @@
url += ',tags';
} else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
url += ',commands';
} else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
url += ',dashboards';
}
return url;
},
@@ -706,6 +711,9 @@
this._mapRoute(RoutePattern.REPO_ACCESS,
'_handleRepoAccessRoute');
this._mapRoute(RoutePattern.REPO_DASHBOARDS,
'_handleRepoDashboardsRoute');
this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
'_handleBranchListOffsetRoute');
@@ -923,12 +931,22 @@
if (titleParam) {
title = titleParam[1];
}
// Dashboards support a foreach param which adds a base query to any
// additional query.
const forEachParam = queryParams.find(
elem => elem[0].toLowerCase() === 'foreach');
let forEachQuery = null;
if (forEachParam) {
forEachQuery = forEachParam[1];
}
const sectionParams = queryParams.filter(
elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title');
elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title'
&& elem[0].toLowerCase() !== 'foreach');
const sections = sectionParams.map(elem => {
const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
return {
name: elem[0],
query: elem[1],
query,
};
});
@@ -1034,6 +1052,14 @@
});
},
_handleRepoDashboardsRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
repo: data.params[0],
});
},
_handleBranchListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,

View File

@@ -160,6 +160,7 @@ limitations under the License.
'_handleProjectDashboardRoute',
'_handleProjectsOldRoute',
'_handleRepoAccessRoute',
'_handleRepoDashboardsRoute',
'_handleRepoListFilterOffsetRoute',
'_handleRepoListFilterRoute',
'_handleRepoListOffsetRoute',
@@ -184,7 +185,6 @@ limitations under the License.
const shouldNotRequireAuth = unauthenticatedHandlers
.concat(selfAuthenticatingHandlers);
shouldNotRequireAuth.sort();
assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth);
});
@@ -850,6 +850,25 @@ limitations under the License.
});
});
});
test('custom dashboard with foreach', () => {
const data = {canonicalPath: '/dashboard/', params: {0: ''}};
return element._handleCustomDashboardRoute(data,
'?a=b&c&d=&=e&foreach=is:open')
.then(() => {
assert.isFalse(redirectToLoginStub.called);
assert.isFalse(redirectStub.called);
assert.isTrue(setParamsStub.calledOnce);
assert.deepEqual(setParamsStub.lastCall.args[0], {
view: Gerrit.Nav.View.DASHBOARD,
user: 'self',
sections: [
{name: 'a', query: 'is:open b'},
],
title: 'Custom Dashboard',
});
});
});
});
suite('group routes', () => {

View File

@@ -253,12 +253,19 @@
},
getRepoAccess(repo) {
// TODO: Rename rest api from project to repo
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._fetchSharedCacheURL(
'/access/?project=' + encodeURIComponent(repo));
},
getRepoDashboards(repo) {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.
return this._fetchSharedCacheURL(
`/projects/${encodeURIComponent(repo)}/dashboards?inherited`);
},
saveRepoConfig(repo, config, opt_errFn, opt_ctx) {
// TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
// supports it.

View File

@@ -38,6 +38,9 @@ limitations under the License.
padding: .3em .5em;
text-align: left;
}
.genericList .groupHeader {
background-color: #eee;
}
.genericList a {
color: var(--default-text-color);
text-decoration: none;

View File

@@ -47,6 +47,7 @@ limitations under the License.
'admin/gr-repo-access/gr-repo-access_test.html',
'admin/gr-repo-command/gr-repo-command_test.html',
'admin/gr-repo-commands/gr-repo-commands_test.html',
'admin/gr-repo-dashboards/gr-repo-dashboards_test.html',
'admin/gr-repo-detail-list/gr-repo-detail-list_test.html',
'admin/gr-repo-list/gr-repo-list_test.html',
'admin/gr-repo/gr-repo_test.html',