PolyGerrit: Implement /admin/groups/<group>

Bug: Issue 6325
Change-Id: I60c50a6c4b25597323781f481159d50a85eebbc5
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index 3d26b55..25b0015 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -28,6 +28,7 @@
 <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 @@
         <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]]">
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 4a54e18..0472f6f 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -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();
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index f0f1aa8..746b7bb 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -138,7 +138,7 @@
     });
 
     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 @@
         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>
\ No newline at end of file
+</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
new file mode 100644
index 0000000..a80790a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
new file mode 100644
index 0000000..a91ad2e
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -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;
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
new file mode 100644
index 0000000..5fc9527
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -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>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 378744f..eab2c31 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -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 = {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 989b383..2d7ef07 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -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');
     },
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 5c00d1d..e503812 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -37,6 +37,7 @@
     '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',