PolyGerrit: Implement /admin/groups/<group>

Bug: Issue 6325
Change-Id: I60c50a6c4b25597323781f481159d50a85eebbc5
This commit is contained in:
Paladox none
2017-06-18 20:21:37 +00:00
parent 2d67b7c5a5
commit a4b816b60c
9 changed files with 622 additions and 12 deletions

View File

@@ -28,6 +28,7 @@ limitations under the License.
<link rel="import" href="../gr-create-project-dialog/gr-create-project-dialog.html">
<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
<link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html">
<link rel="import" href="../gr-group/gr-group.html">
<link rel="import" href="../gr-plugin-list/gr-plugin-list.html">
<link rel="import" href="../gr-project/gr-project.html">
<link rel="import" href="../gr-project-detail-list/gr-project-detail-list.html">
@@ -79,6 +80,13 @@ limitations under the License.
<gr-project project="[[params.project]]"></gr-project>
</main>
</template>
<template is="dom-if" if="[[_showGroup]]" restamp="true">
<main>
<gr-group
group-id="[[params.groupId]]"
on-name-changed="_updateGroupName"></gr-group>
</main>
</template>
<template is="dom-if" if="[[_showGroupList]]" restamp="true">
<main class="table">
<gr-admin-group-list class="table" params="[[params]]">

View File

@@ -44,12 +44,18 @@
path: String,
adminView: String,
_project: String,
_projectName: String,
_groupId: {
type: Number,
observer: '_computeGroupName',
},
_groupName: String,
_filteredLinks: Array,
_showDownload: {
type: Boolean,
value: false,
},
_showGroup: Boolean,
_showGroupList: Boolean,
_showProjectMain: Boolean,
_showProjectList: Boolean,
@@ -90,27 +96,35 @@
const linkCopy = Object.assign({}, link);
linkCopy.children = linkCopy.children ?
linkCopy.children.filter(filterFn) : [];
if (linkCopy.name === 'Projects' && this._project) {
if (linkCopy.name === 'Projects' && this._projectName) {
linkCopy.subsection = {
name: `${this._project}`,
name: this._projectName,
view: 'gr-project',
url: `/admin/projects/${this.encodeURL(this._project, true)}`,
url: `/admin/projects/${this.encodeURL(this._projectName, true)}`,
children: [{
name: 'Branches',
detailType: 'branches',
view: 'gr-project-detail-list',
url: `/admin/projects/${this.encodeURL(this._project, true)}` +
',branches',
url: `/admin/projects/` +
`${this.encodeURL(this._projectName, true)},branches`,
},
{
name: 'Tags',
detailType: 'tags',
view: 'gr-project-detail-list',
url: `/admin/projects/${this.encodeURL(this._project, true)}` +
',tags',
url: `/admin/projects/` +
`${this.encodeURL(this._projectName, true)},tags`,
}],
};
}
if (linkCopy.name === 'Groups' && this._groupId && this._groupName) {
linkCopy.subsection = {
name: this._groupName,
view: 'gr-group',
url: `/admin/groups/${this.encodeURL(this._groupId, true)}`,
children: [],
};
}
filteredLinks.push(linkCopy);
}
return filteredLinks;
@@ -127,6 +141,7 @@
},
_paramsChanged(params) {
this.set('_showGroup', params.adminView === 'gr-group');
this.set('_showGroupList', params.adminView === 'gr-admin-group-list');
this.set('_showProjectMain', params.adminView === 'gr-project');
this.set('_showProjectList',
@@ -134,8 +149,13 @@
this.set('_showProjectDetailList',
params.adminView === 'gr-project-detail-list');
this.set('_showPluginList', params.adminView === 'gr-plugin-list');
if (params.project !== this._project) {
this._project = params.project || '';
if (params.project !== this._projectName) {
this._projectName = params.project || '';
// Reloads the admin menu.
this.reload();
}
if (params.groupId !== this._groupId) {
this._groupId = params.groupId || '';
// Reloads the admin menu.
this.reload();
}
@@ -167,5 +187,18 @@
}
return itemView === params.adminView ? 'selected' : '';
},
_computeGroupName(groupId) {
if (!groupId) { return ''; }
this.$.restAPI.getGroupConfig(groupId).then(group => {
this._groupName = group.name;
this.reload();
});
},
_updateGroupName(e) {
this._groupName = e.detail.name;
this.reload();
},
});
})();

View File

@@ -138,7 +138,7 @@ limitations under the License.
});
test('Project shows up in nav', done => {
element._project = 'Test Project';
element._projectName = 'Test Project';
sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
return Promise.resolve({
createGroup: true,
@@ -180,5 +180,36 @@ limitations under the License.
adminView: 'gr-project'};
assert.equal(element.reload.callCount, 2);
});
test('Nav is reloaded when group changes', () => {
sandbox.stub(element, '_computeGroupName');
sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
return Promise.resolve({
createGroup: true,
createProject: true,
viewPlugins: true,
});
});
sandbox.stub(element.$.restAPI, 'getAccount', () => {
return Promise.resolve({_id: 1});
});
sandbox.stub(element, 'reload');
element.params = {groupId: '1', adminView: 'gr-group'};
assert.equal(element.reload.callCount, 1);
});
test('Nav is reloaded when group name changes', done => {
const newName = 'newName';
sandbox.stub(element, '_computeGroupName');
sandbox.stub(element, 'reload', () => {
assert.equal(element._groupName, newName);
assert.isTrue(element.reload.called);
done();
});
element.params = {group: 1, adminView: 'gr-group'};
element._groupName = 'oldName';
flushAsynchronousOperations();
element.$$('gr-group').fire('name-changed', {name: newName});
});
});
</script>
</script>

View File

@@ -0,0 +1,149 @@
<!--
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.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<dom-module id="gr-group">
<template>
<style include="shared-styles">
main {
margin: 2em 1em;
}
h3.edited:after {
color: #444;
content: ' *';
}
.loading {
display: none;
}
#loading.loading {
display: block;
}
#loading:not(.loading) {
display: none;
}
.inputUpdateBtn {
margin-top: .3em;
}
</style>
<style include="gr-form-styles"></style>
<main class="gr-form-styles read-only">
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<h1 id="Title">[[_groupName]]</h1>
<h2 id="configurations">General</h2>
<div id="form">
<fieldset>
<h3 id="groupUUID">Group UUID</h3>
<fieldset>
<gr-copy-clipboard
text="[[_groupConfig.id]]"></gr-copy-clipboard>
</fieldset>
<h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
Group Name
</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="groupNameInput"
text="{{_groupConfig.name}}"
disabled$="[[_groupOwner]]"></gr-autocomplete>
</span>
<gr-button
id="inputUpdateNameBtn"
on-tap="_handleSaveName"
disabled$="[[_computeButtonDisabled(_groupOwner, _rename)]]">
Rename Group</gr-button>
</fieldset>
<h3 class$="[[_computeHeaderClass(_owner)]]">
Owners
</h3>
<fieldset>
<span class="value">
<gr-autocomplete
text="{{_groupConfig.owner}}"
query="[[_query]]"
disabled$="[[_groupOwner]]">
</gr-autocomplete>
</span>
<gr-button
on-tap="_handleSaveOwner"
disabled$="[[_computeButtonDisabled(_groupOwner, _owner)]]">
Change Owners</gr-button>
</fieldset>
<h3 class$="[[_computeHeaderClass(_description)]]">
Description
</h3>
<fieldset>
<div>
<iron-autogrow-textarea
class="description"
autocomplete="on"
bind-value="{{_groupConfig.description}}"
disabled$="[[_groupOwner]]"></iron-autogrow-textarea>
</div>
<gr-button
on-tap="_handleSaveDescription"
disabled$=
"[[_computeButtonDisabled(_groupOwner, _description)]]">
Save Description
</gr-button>
</fieldset>
<h3 id="options" class$="[[_computeHeaderClass(_options)]]">
Group Options
</h3>
<fieldset id="visableToAll">
<section>
<span class="title">
Make group visible to all registered users
</span>
<span class="value">
<gr-select
bind-value="{{_groupConfig.options.visible_to_all}}">
<select disabled$="[[_groupOwner]]">
<template is="dom-repeat" items="[[_submitTypes]]">
<option value="[[item.value]]">[[item.label]]</option>
</template>
</select>
</gr-select>
</span>
</section>
<gr-button
on-tap="_handleSaveOptions"
disabled$=
"[[_computeButtonDisabled(_groupOwner, _options)]]">
Save Group Options
</gr-button>
</fieldset>
</fieldset>
</div>
</div>
</main>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-group.js"></script>
</dom-module>

View File

@@ -0,0 +1,211 @@
// 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';
const OPTIONS = {
submitFalse: {
value: false,
label: 'False',
},
submitTrue: {
value: true,
label: 'True',
},
};
Polymer({
is: 'gr-group',
/**
* Fired when the group name changes.
*
* @event name-changed
*/
properties: {
groupId: Number,
_rename: {
type: Boolean,
value: false,
},
_description: {
type: Boolean,
value: false,
},
_owner: {
type: Boolean,
value: false,
},
_options: {
type: Boolean,
value: false,
},
_loading: {
type: Boolean,
value: true,
},
_loggedIn: {
type: Boolean,
value: false,
observer: '_loggedInChanged',
},
_groupConfig: Object,
_groupName: Object,
_groupOwner: {
type: Boolean,
value: false,
},
_submitTypes: {
type: Array,
value() {
return Object.values(OPTIONS);
},
},
_query: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
},
observers: [
'_handleConfigName(_groupConfig.name)',
'_handleConfigOwner(_groupConfig.owner)',
'_handleConfigDescription(_groupConfig.description)',
'_handleConfigOptions(_groupConfig.options.visible_to_all)',
],
attached() {
this._loadGroup();
},
_loadGroup() {
if (!this.groupId) { return; }
return this.$.restAPI.getGroupConfig(this.groupId).then(
config => {
this._groupConfig = config;
this._groupName = config.name;
this._loading = false;
this.$.restAPI.getIsGroupOwner(config.name).then(
configs => {
if (Object.keys(configs).length === 0 &&
configs.constructor === Object) {
this._groupOwner = true;
}
});
});
},
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
},
_loggedInChanged(_loggedIn) {
if (!_loggedIn) { return; }
},
_isLoading() {
return this._loading || this._loading === undefined;
},
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
},
_handleSaveName() {
return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name)
.then(config => {
if (config.status === 200) {
this._groupName = this._groupConfig.name;
this.fire('name-changed', {name: this._groupConfig.name});
this._rename = false;
}
});
},
_handleSaveOwner() {
return this.$.restAPI.saveGroupOwner(this.groupId,
this._groupConfig.owner).then(config => {
this._owner = false;
});
},
_handleSaveDescription() {
return this.$.restAPI.saveGroupDescription(this.groupId,
this._groupConfig.description).then(config => {
this._description = false;
});
},
_handleSaveOptions() {
let options;
// The value is in string so we have to convert it to a boolean.
if (this._groupConfig.options.visible_to_all) {
options = {visible_to_all: true};
} else if (!this._groupConfig.options.visible_to_all) {
options = {visible_to_all: false};
}
return this.$.restAPI.saveGroupOptions(this.groupId,
options).then(config => {
this._options = false;
});
},
_handleConfigName() {
if (this._isLoading()) { return; }
this._rename = true;
},
_handleConfigOwner() {
if (this._isLoading()) { return; }
this._owner = true;
},
_handleConfigDescription() {
if (this._isLoading()) { return; }
this._description = true;
},
_handleConfigOptions() {
if (this._isLoading()) { return; }
this._options = true;
},
_computeButtonDisabled(options, option) {
return options || !option;
},
_computeHeaderClass(configChanged) {
return configChanged ? 'edited' : '';
},
_getGroupSuggestions(input) {
return this.$.restAPI.getSuggestedGroups(input)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: response[key],
});
}
return groups;
});
},
});
})();

View File

@@ -0,0 +1,124 @@
<!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-group</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-group.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-group></gr-group>
</template>
</test-fixture>
<script>
suite('gr-group tests', () => {
let element;
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(true); },
getGroupConfig() {
return Promise.resolve({
id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
options: {
},
description: 'Gerrit Site Administrators',
group_id: 1,
owner: 'Administrators',
owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
});
},
});
element = fixture('basic');
});
teardown(() => {
sandbox.restore();
});
test('loading displays before group config is loaded', () => {
assert.isTrue(element.$.loading.classList.contains('loading'));
assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
assert.isTrue(element.$.loadedContent.classList.contains('loading'));
assert.isTrue(getComputedStyle(element.$.loadedContent)
.display === 'none');
});
test('rename group', done => {
const groupName = 'test-group';
const groupName2 = 'test-group2';
element.groupId = 1;
element._groupConfig = {
name: groupName,
};
element._groupName = groupName;
sandbox.stub(element.$.restAPI, 'getIsGroupOwner', () => {
return Promise.resolve({is_owner: true});
});
sandbox.stub(element.$.restAPI, 'saveGroupName', () => {
return Promise.resolve({status: 200});
});
const button = element.$.inputUpdateNameBtn;
element._loadGroup().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
element.$.groupNameInput.text = groupName2;
assert.isFalse(button.hasAttribute('disabled'));
assert.isTrue(element.$.groupName.classList.contains('edited'));
element._handleSaveName().then(() => {
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(element.$.Title.classList.contains('edited'));
assert.equal(element._groupName, groupName2);
done();
});
});
});
test('test fire event', done => {
element._groupConfig = {
name: 'test-group',
};
sandbox.stub(element.$.restAPI, 'saveGroupName')
.returns(Promise.resolve({status: 200}));
const showStub = sandbox.stub(element, 'fire');
element._handleSaveName()
.then(() => {
assert.isTrue(showStub.called);
done();
});
});
});
</script>

View File

@@ -139,6 +139,18 @@
});
});
// Matches /admin/groups/<group>,info (backwords compat with gwtui)
// Redirects to /admin/groups/<group>
page(/^\/admin\/groups\/(.+),info$/, loadUser, data => {
restAPI.getLoggedIn().then(loggedIn => {
if (loggedIn) {
page.redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
} else {
redirectToLogin(data.canonicalPath);
}
});
});
// Matches /admin/groups[,<offset>][/].
page(/^\/admin\/groups(,(\d+))?(\/)?$/, loadUser, data => {
restAPI.getLoggedIn().then(loggedIn => {
@@ -184,6 +196,21 @@
});
});
// Matches /admin/groups/<group>
page(/^\/admin\/groups\/(.+)$/, loadUser, data => {
restAPI.getLoggedIn().then(loggedIn => {
if (loggedIn) {
app.params = {
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-group',
groupId: data.params[0],
};
} else {
redirectToLogin(data.canonicalPath);
}
});
});
// Matches /admin/projects/<project>,branches[,<offset>].
page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => {
app.params = {

View File

@@ -257,6 +257,32 @@
revision, opt_errFn, opt_ctx);
},
getIsGroupOwner(groupId) {
const encodeId = encodeURIComponent(groupId);
return this._fetchSharedCacheURL('/groups/?owned&q=' + encodeId);
},
saveGroupName(groupId, name) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/name`, {name});
},
saveGroupOwner(groupId, ownerId) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/owner`, {owner: ownerId});
},
saveGroupDescription(groupId, description) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/description`,
{description});
},
saveGroupOptions(groupId, options) {
const encodeId = encodeURIComponent(groupId);
return this.send('PUT', `/groups/${encodeId}/options`, options);
},
getVersion() {
return this._fetchSharedCacheURL('/config/server/version');
},

View File

@@ -37,6 +37,7 @@ limitations under the License.
'admin/gr-create-group-dialog/gr-create-group-dialog_test.html',
'admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html',
'admin/gr-create-project-dialog/gr-create-project-dialog_test.html',
'admin/gr-group/gr-group_test.html',
'admin/gr-plugin-list/gr-plugin-list_test.html',
'admin/gr-project/gr-project_test.html',
'admin/gr-project-detail-list/gr-project-detail-list_test.html',