Merge "Fix change-list-item having grown from 28px to 29px height"
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 1038629..5e944c2 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -94,7 +94,6 @@
 # so template tests pass.
 # TODO: fix problems reported by template checker in these files.
 ignore_templates_list = [
-    "elements/admin/gr-admin-view/gr-admin-view_html.ts",
     "elements/admin/gr-permission/gr-permission_html.ts",
     "elements/admin/gr-repo-access/gr-repo-access_html.ts",
     "elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index 2c2405b..762bffc 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -14,9 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/gr-menu-page-styles';
-import '../../../styles/gr-page-nav-styles';
-import '../../../styles/shared-styles';
+
 import '../../shared/gr-dropdown-list/gr-dropdown-list';
 import '../../shared/gr-icons/gr-icons';
 import '../../shared/gr-page-nav/gr-page-nav';
@@ -31,8 +29,6 @@
 import '../gr-repo-dashboards/gr-repo-dashboards';
 import '../gr-repo-detail-list/gr-repo-detail-list';
 import '../gr-repo-list/gr-repo-list';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-admin-view_html';
 import {getBaseUrl} from '../../../utils/url-util';
 import {
   GerritNav,
@@ -46,7 +42,6 @@
   NavLink,
   SubsectionInterface,
 } from '../../../utils/admin-nav-util';
-import {customElement, observe, property} from '@polymer/decorators';
 import {
   AppElementAdminParams,
   AppElementGroupParams,
@@ -62,6 +57,11 @@
 import {ValueChangeDetail} from '../../shared/gr-dropdown-list/gr-dropdown-list';
 import {getAppContext} from '../../../services/app-context';
 import {GerritView} from '../../../services/router/router-model';
+import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
 
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
@@ -90,11 +90,7 @@
 }
 
 @customElement('gr-admin-view')
-export class GrAdminView extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrAdminView extends LitElement {
   private account?: AccountDetailInfo;
 
   @property({type: Object})
@@ -106,68 +102,25 @@
   @property({type: String})
   adminView?: string;
 
-  @property({type: String})
-  _breadcrumbParentName?: string;
+  @state() private breadcrumbParentName?: string;
 
-  @property({type: String})
-  _repoName?: RepoName;
+  // private but used in test
+  @state() repoName?: RepoName;
 
-  @property({type: String, observer: '_computeGroupName'})
-  _groupId?: GroupId;
+  // private but used in test
+  @state() groupId?: GroupId;
 
-  @property({type: Boolean})
-  _groupIsInternal?: boolean;
+  // private but used in test
+  @state() groupIsInternal?: boolean;
 
-  @property({type: String})
-  _groupName?: GroupName;
+  // private but used in test
+  @state() groupName?: GroupName;
 
-  @property({type: Boolean})
-  _groupOwner = false;
+  // private but used in test
+  @state() subsectionLinks?: AdminSubsectionLink[];
 
-  @property({type: Array})
-  _subsectionLinks?: AdminSubsectionLink[];
-
-  @property({type: Array})
-  _filteredLinks?: NavLink[];
-
-  @property({type: Boolean})
-  _showDownload = false;
-
-  @property({type: Boolean})
-  _isAdmin = false;
-
-  @property({type: Boolean})
-  _showGroup?: boolean;
-
-  @property({type: Boolean})
-  _showGroupAuditLog?: boolean;
-
-  @property({type: Boolean})
-  _showGroupList?: boolean;
-
-  @property({type: Boolean})
-  _showGroupMembers?: boolean;
-
-  @property({type: Boolean})
-  _showRepoAccess?: boolean;
-
-  @property({type: Boolean})
-  _showRepoCommands?: boolean;
-
-  @property({type: Boolean})
-  _showRepoDashboards?: boolean;
-
-  @property({type: Boolean})
-  _showRepoDetailList?: boolean;
-
-  @property({type: Boolean})
-  _showRepoMain?: boolean;
-
-  @property({type: Boolean})
-  _showRepoList?: boolean;
-
-  @property({type: Boolean})
-  _showPluginList?: boolean;
+  // private but used in test
+  @state() filteredLinks?: NavLink[];
 
   // private but used in the tests
   readonly jsAPI = getAppContext().jsApiService;
@@ -179,201 +132,479 @@
     this.reload();
   }
 
-  reload() {
+  static override get styles() {
+    return [
+      sharedStyles,
+      menuPageStyles,
+      pageNavStyles,
+      css`
+        .breadcrumbText {
+          /* Same as dropdown trigger so chevron spacing is consistent. */
+          padding: 5px 4px;
+        }
+        iron-icon {
+          margin: 0 var(--spacing-xs);
+        }
+        .breadcrumb {
+          align-items: center;
+          display: flex;
+        }
+        .mainHeader {
+          align-items: baseline;
+          border-bottom: 1px solid var(--border-color);
+          display: flex;
+        }
+        .selectText {
+          display: none;
+        }
+        .selectText.show {
+          display: inline-block;
+        }
+        .main.breadcrumbs:not(.table) {
+          margin-top: var(--spacing-l);
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <gr-page-nav class="navStyles">
+        <ul class="sectionContent">
+          ${this.filteredLinks?.map(item => this.renderAdminNav(item))}
+        </ul>
+      </gr-page-nav>
+      ${this.renderSubsectionLinks()} ${this.renderRepoList()}
+      ${this.renderGroupList()} ${this.renderPluginList()}
+      ${this.renderRepoMain()} ${this.renderGroup()}
+      ${this.renderGroupMembers()} ${this.renderGroupAuditLog()}
+      ${this.renderRepoDetailList()} ${this.renderRepoCommands()}
+      ${this.renderRepoAccess()} ${this.renderRepoDashboards()}
+    `;
+  }
+
+  private renderAdminNav(item: NavLink) {
+    return html`
+      <li class="sectionTitle ${this.computeSelectedClass(item.view)}">
+        <a class="title" href="${this.computeLinkURL(item)}" rel="noopener"
+          >${item.name}</a
+        >
+      </li>
+      ${item.children?.map(child => this.renderAdminNavChild(child))}
+      ${this.renderAdminNavSubsection(item)}
+    `;
+  }
+
+  private renderAdminNavChild(child: SubsectionInterface) {
+    return html`
+      <li class="${this.computeSelectedClass(child.view)}">
+        <a href="${this.computeLinkURL(child)}" rel="noopener">${child.name}</a>
+      </li>
+    `;
+  }
+
+  private renderAdminNavSubsection(item: NavLink) {
+    if (!item.subsection) return;
+
+    return html`
+      <!--If a section has a subsection, render that.-->
+      <li class="${this.computeSelectedClass(item.subsection.view)}">
+        ${this.renderAdminNavSubsectionUrl(item.subsection)}
+      </li>
+      <!--Loop through the links in the sub-section.-->
+      ${item.subsection?.children?.map(child =>
+        this.renderAdminNavSubsectionChild(child)
+      )}
+    `;
+  }
+
+  private renderAdminNavSubsectionUrl(subsection?: SubsectionInterface) {
+    if (!subsection!.url) return html`${subsection!.name}`;
+
+    return html`
+      <a class="title" href="${this.computeLinkURL(subsection)}" rel="noopener">
+        ${subsection!.name}</a
+      >
+    `;
+  }
+
+  private renderAdminNavSubsectionChild(child: SubsectionInterface) {
+    return html`
+      <li
+        class="subsectionItem ${this.computeSelectedClass(
+          child.view,
+          child.detailType
+        )}"
+      >
+        <a href="${this.computeLinkURL(child)}">${child.name}</a>
+      </li>
+    `;
+  }
+
+  private renderSubsectionLinks() {
+    if (!this.subsectionLinks?.length) return;
+
+    return html`
+      <section class="mainHeader">
+        <span class="breadcrumb">
+          <span class="breadcrumbText">${this.breadcrumbParentName}</span>
+          <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+        </span>
+        <gr-dropdown-list
+          id="pageSelect"
+          lowercase
+          value=${this.computeSelectValue()}
+          .items=${this.subsectionLinks}
+          @value-change=${this.handleSubsectionChange}
+        >
+        </gr-dropdown-list>
+      </section>
+    `;
+  }
+
+  private renderRepoList() {
+    const params = this.params as AppElementAdminParams;
+    if (
+      !(
+        params?.view === GerritView.ADMIN &&
+        params?.adminView === 'gr-repo-list'
+      )
+    )
+      return;
+
+    return html`
+      <div class="main table">
+        <gr-repo-list class="table" .params=${params}></gr-repo-list>
+      </div>
+    `;
+  }
+
+  private renderGroupList() {
+    const params = this.params as AppElementAdminParams;
+    if (
+      !(
+        params?.view === GerritView.ADMIN &&
+        params?.adminView === 'gr-admin-group-list'
+      )
+    )
+      return;
+
+    return html`
+      <div class="main table">
+        <gr-admin-group-list class="table" .params=${params}>
+        </gr-admin-group-list>
+      </div>
+    `;
+  }
+
+  private renderPluginList() {
+    const params = this.params as AppElementAdminParams;
+    if (
+      !(
+        params?.view === GerritView.ADMIN &&
+        params?.adminView === 'gr-plugin-list'
+      )
+    )
+      return;
+
+    return html`
+      <div class="main table">
+        <gr-plugin-list class="table" .params=${params}></gr-plugin-list>
+      </div>
+    `;
+  }
+
+  private renderRepoMain() {
+    const params = this.params as AppElementRepoParams;
+    if (
+      !(
+        params?.view === GerritView.REPO &&
+        (!params?.detail || params?.detail === RepoDetailView.GENERAL)
+      )
+    )
+      return;
+
+    return html`
+      <div class="main breadcrumbs">
+        <gr-repo .repo=${params.repo}></gr-repo>
+      </div>
+    `;
+  }
+
+  private renderGroup() {
+    const params = this.params as AppElementGroupParams;
+    if (!(params?.view === GerritView.GROUP && !params?.detail)) return;
+
+    return html`
+      <div class="main breadcrumbs">
+        <gr-group
+          .groupId=${params.groupId}
+          @name-changed=${(e: CustomEvent<GroupNameChangedDetail>) => {
+            this.updateGroupName(e);
+          }}
+        ></gr-group>
+      </div>
+    `;
+  }
+
+  private renderGroupMembers() {
+    const params = this.params as AppElementGroupParams;
+    if (
+      !(
+        params?.view === GerritView.GROUP &&
+        params?.detail === GroupDetailView.MEMBERS
+      )
+    )
+      return;
+
+    return html`
+      <div class="main breadcrumbs">
+        <gr-group-members .groupId=${params.groupId}></gr-group-members>
+      </div>
+    `;
+  }
+
+  private renderGroupAuditLog() {
+    const params = this.params as AppElementGroupParams;
+    if (
+      !(
+        params?.view === GerritView.GROUP &&
+        params?.detail === GroupDetailView.LOG
+      )
+    )
+      return;
+
+    return html`
+      <div class="main table breadcrumbs">
+        <gr-group-audit-log
+          class="table"
+          .groupId=${params.groupId}
+        ></gr-group-audit-log>
+      </div>
+    `;
+  }
+
+  private renderRepoDetailList() {
+    const params = this.params as AppElementRepoParams;
+    if (
+      !(
+        params?.view === GerritView.REPO &&
+        (params?.detail === RepoDetailView.BRANCHES ||
+          params?.detail === RepoDetailView.TAGS)
+      )
+    )
+      return;
+
+    return html`
+      <div class="main table breadcrumbs">
+        <gr-repo-detail-list
+          class="table"
+          .params=${params}
+        ></gr-repo-detail-list>
+      </div>
+    `;
+  }
+
+  private renderRepoCommands() {
+    const params = this.params as AppElementRepoParams;
+    if (
+      !(
+        params?.view === GerritView.REPO &&
+        params?.detail === RepoDetailView.COMMANDS
+      )
+    )
+      return;
+
+    return html`
+      <div class="main breadcrumbs">
+        <gr-repo-commands .repo=${params.repo}></gr-repo-commands>
+      </div>
+    `;
+  }
+
+  private renderRepoAccess() {
+    const params = this.params as AppElementRepoParams;
+    if (
+      !(
+        params?.view === GerritView.REPO &&
+        params?.detail === RepoDetailView.ACCESS
+      )
+    )
+      return;
+
+    return html`
+      <div class="main breadcrumbs">
+        <gr-repo-access
+          .path=${this.path}
+          .repo=${params.repo}
+        ></gr-repo-access>
+      </div>
+    `;
+  }
+
+  private renderRepoDashboards() {
+    const params = this.params as AppElementRepoParams;
+    if (
+      !(
+        params?.view === GerritView.REPO &&
+        params?.detail === RepoDetailView.DASHBOARDS
+      )
+    )
+      return;
+
+    return html`
+      <div class="main table breadcrumbs">
+        <gr-repo-dashboards .repo=${params.repo}></gr-repo-dashboards>
+      </div>
+    `;
+  }
+
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('params')) {
+      this.paramsChanged();
+    }
+
+    if (changedProperties.has('groupId')) {
+      this.computeGroupName();
+    }
+  }
+
+  async reload() {
     const promises: [Promise<AccountDetailInfo | undefined>, Promise<void>] = [
       this.restApiService.getAccount(),
       getPluginLoader().awaitPluginsLoaded(),
     ];
-    return Promise.all(promises).then(result => {
-      this.account = result[0];
-      let options: AdminNavLinksOption | undefined = undefined;
-      if (this._repoName) {
-        options = {repoName: this._repoName};
-      } else if (this._groupId) {
-        options = {
-          groupId: this._groupId,
-          groupName: this._groupName,
-          groupIsInternal: this._groupIsInternal,
-          isAdmin: this._isAdmin,
-          groupOwner: this._groupOwner,
+    const result = await Promise.all(promises);
+    this.account = result[0];
+    let options: AdminNavLinksOption | undefined = undefined;
+    if (this.repoName) {
+      options = {repoName: this.repoName};
+    } else if (this.groupId) {
+      const isAdmin = await this.restApiService.getIsAdmin();
+      const isOwner = await this.restApiService.getIsGroupOwner(this.groupName);
+      options = {
+        groupId: this.groupId,
+        groupName: this.groupName,
+        groupIsInternal: this.groupIsInternal,
+        isAdmin,
+        groupOwner: isOwner,
+      };
+    }
+
+    const res = await getAdminLinks(
+      this.account,
+      () =>
+        this.restApiService.getAccountCapabilities().then(capabilities => {
+          if (!capabilities) {
+            throw new Error('getAccountCapabilities returns undefined');
+          }
+          return capabilities;
+        }),
+      () => this.jsAPI.getAdminMenuLinks(),
+      options
+    );
+    this.filteredLinks = res.links;
+    this.breadcrumbParentName = res.expandedSection
+      ? res.expandedSection.name
+      : '';
+
+    if (!res.expandedSection) {
+      this.subsectionLinks = [];
+      return;
+    }
+    this.subsectionLinks = [res.expandedSection]
+      .concat(res.expandedSection.children ?? [])
+      .map(section => {
+        return {
+          text: !section.detailType ? 'Home' : section.name,
+          value: section.view + (section.detailType ?? ''),
+          view: section.view,
+          url: section.url,
+          detailType: section.detailType,
+          parent: this.groupId ?? this.repoName,
         };
-      }
-
-      return getAdminLinks(
-        this.account,
-        () =>
-          this.restApiService.getAccountCapabilities().then(capabilities => {
-            if (!capabilities) {
-              throw new Error('getAccountCapabilities returns undefined');
-            }
-            return capabilities;
-          }),
-        () => this.jsAPI.getAdminMenuLinks(),
-        options
-      ).then(res => {
-        this._filteredLinks = res.links;
-        this._breadcrumbParentName = res.expandedSection
-          ? res.expandedSection.name
-          : '';
-
-        if (!res.expandedSection) {
-          this._subsectionLinks = [];
-          return;
-        }
-        this._subsectionLinks = [res.expandedSection]
-          .concat(res.expandedSection.children ?? [])
-          .map(section => {
-            return {
-              text: !section.detailType ? 'Home' : section.name,
-              value: section.view + (section.detailType ?? ''),
-              view: section.view,
-              url: section.url,
-              detailType: section.detailType,
-              parent: this._groupId ?? this._repoName,
-            };
-          });
       });
-    });
   }
 
-  _computeSelectValue(params: AdminViewParams) {
-    if (!params || !params.view) return;
-    return `${params.view}${getAdminViewParamsDetail(params) ?? ''}`;
+  private computeSelectValue() {
+    if (!this.params?.view) return;
+    return `${this.params.view}${getAdminViewParamsDetail(this.params) ?? ''}`;
   }
 
-  _selectedIsCurrentPage(selected: AdminSubsectionLink) {
+  // private but used in test
+  selectedIsCurrentPage(selected: AdminSubsectionLink) {
     if (!this.params) return false;
 
     return (
-      selected.parent === (this._repoName ?? this._groupId) &&
+      selected.parent === (this.repoName ?? this.groupId) &&
       selected.view === this.params.view &&
       selected.detailType === getAdminViewParamsDetail(this.params)
     );
   }
 
-  _handleSubsectionChange(e: CustomEvent<ValueChangeDetail>) {
-    if (!this._subsectionLinks) return;
+  // private but used in test
+  handleSubsectionChange(e: CustomEvent<ValueChangeDetail>) {
+    if (!this.subsectionLinks) return;
 
-    // The GrDropdownList items are _subsectionLinks, so find(...) always return
-    // an item _subsectionLinks and never returns undefined
-    const selected = this._subsectionLinks.find(
+    // The GrDropdownList items are subsectionLinks, so find(...) always return
+    // an item subsectionLinks and never returns undefined
+    const selected = this.subsectionLinks.find(
       section => section.value === e.detail.value
     )!;
 
     // This is when it gets set initially.
-    if (this._selectedIsCurrentPage(selected)) return;
+    if (this.selectedIsCurrentPage(selected)) return;
     if (selected.url === undefined) return;
     GerritNav.navigateToRelativeUrl(selected.url);
   }
 
-  @observe('params')
-  _paramsChanged(params: AdminViewParams) {
-    this.set('_showGroup', params.view === GerritView.GROUP && !params.detail);
-    this.set(
-      '_showGroupAuditLog',
-      params.view === GerritView.GROUP && params.detail === GroupDetailView.LOG
-    );
-    this.set(
-      '_showGroupMembers',
-      params.view === GerritView.GROUP &&
-        params.detail === GroupDetailView.MEMBERS
-    );
+  private async paramsChanged() {
+    if (this.needsReload()) await this.reload();
+  }
 
-    this.set(
-      '_showGroupList',
-      params.view === GerritView.ADMIN &&
-        params.adminView === 'gr-admin-group-list'
-    );
+  needsReload() {
+    if (!this.params) return;
 
-    this.set(
-      '_showRepoAccess',
-      params.view === GerritView.REPO && params.detail === RepoDetailView.ACCESS
-    );
-    this.set(
-      '_showRepoCommands',
-      params.view === GerritView.REPO &&
-        params.detail === RepoDetailView.COMMANDS
-    );
-    this.set(
-      '_showRepoDetailList',
-      params.view === GerritView.REPO &&
-        (params.detail === RepoDetailView.BRANCHES ||
-          params.detail === RepoDetailView.TAGS)
-    );
-    this.set(
-      '_showRepoDashboards',
-      params.view === GerritView.REPO &&
-        params.detail === RepoDetailView.DASHBOARDS
-    );
-    this.set(
-      '_showRepoMain',
-      params.view === GerritView.REPO &&
-        (!params.detail || params.detail === RepoDetailView.GENERAL)
-    );
-    this.set(
-      '_showRepoList',
-      params.view === GerritView.ADMIN && params.adminView === 'gr-repo-list'
-    );
-
-    this.set(
-      '_showPluginList',
-      params.view === GerritView.ADMIN && params.adminView === 'gr-plugin-list'
-    );
-
-    let needsReload = false;
     const newRepoName =
-      params.view === GerritView.REPO ? params.repo : undefined;
-    if (newRepoName !== this._repoName) {
-      this._repoName = newRepoName;
+      this.params.view === GerritView.REPO ? this.params.repo : undefined;
+    if (newRepoName !== this.repoName) {
+      this.repoName = newRepoName;
       // Reloads the admin menu.
-      needsReload = true;
+      return true;
     }
     const newGroupId =
-      params.view === GerritView.GROUP ? params.groupId : undefined;
-    if (newGroupId !== this._groupId) {
-      this._groupId = newGroupId;
+      this.params.view === GerritView.GROUP ? this.params.groupId : undefined;
+    if (newGroupId !== this.groupId) {
+      this.groupId = newGroupId;
       // Reloads the admin menu.
-      needsReload = true;
+      return true;
     }
     if (
-      this._breadcrumbParentName &&
-      (params.view !== GerritView.GROUP || !params.groupId) &&
-      (params.view !== GerritView.REPO || !params.repo)
+      this.breadcrumbParentName &&
+      (this.params.view !== GerritView.GROUP || !this.params.groupId) &&
+      (this.params.view !== GerritView.REPO || !this.params.repo)
     ) {
-      needsReload = true;
+      return true;
     }
-    if (!needsReload) {
-      return;
-    }
-    this.reload();
+
+    return false;
   }
 
-  // TODO (beckysiegel): Update these functions after router abstraction is
-  // updated. They are currently copied from gr-dropdown (and should be
-  // updated there as well once complete).
-  _computeURLHelper(host: string, path: string) {
-    return '//' + host + getBaseUrl() + path;
-  }
-
-  _computeRelativeURL(path: string) {
-    const host = window.location.host;
-    return this._computeURLHelper(host, path);
-  }
-
-  _computeLinkURL(link: NavLink | SubsectionInterface) {
+  // private but used in test
+  computeLinkURL(link?: NavLink | SubsectionInterface) {
     if (!link || typeof link.url === 'undefined') return '';
 
     if ((link as NavLink).target || !(link as NavLink).noBaseUrl) {
       return link.url;
     }
-    return this._computeRelativeURL(link.url);
+    return `//${window.location.host}${getBaseUrl()}${link.url}`;
   }
 
-  _computeSelectedClass(
+  private computeSelectedClass(
     itemView?: GerritView,
-    params?: AdminViewParams,
     detailType?: GroupDetailView | RepoDetailView
   ) {
+    const params = this.params;
     if (!params) return '';
     // Group params are structured differently from admin params. Compute
     // selected differently for groups.
@@ -410,40 +641,23 @@
       : '';
   }
 
-  _computeGroupName(groupId?: GroupId) {
-    if (!groupId) return;
+  // private but used in test
+  async computeGroupName() {
+    if (!this.groupId) return;
 
-    const promises: Array<Promise<void>> = [];
-    this.restApiService.getGroupConfig(groupId).then(group => {
-      if (!group || !group.name) {
-        return;
-      }
+    const group = await this.restApiService.getGroupConfig(this.groupId);
+    if (!group || !group.name) {
+      return;
+    }
 
-      this._groupName = group.name;
-      this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
-      this.reload();
-
-      promises.push(
-        this.restApiService.getIsAdmin().then(isAdmin => {
-          this._isAdmin = !!isAdmin;
-        })
-      );
-
-      promises.push(
-        this.restApiService.getIsGroupOwner(group.name).then(isOwner => {
-          this._groupOwner = isOwner;
-        })
-      );
-
-      return Promise.all(promises).then(() => {
-        this.reload();
-      });
-    });
+    this.groupName = group.name;
+    this.groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
+    await this.reload();
   }
 
-  _updateGroupName(e: CustomEvent<GroupNameChangedDetail>) {
-    this._groupName = e.detail.name;
-    this.reload();
+  private async updateGroupName(e: CustomEvent<GroupNameChangedDetail>) {
+    this.groupName = e.detail.name;
+    await this.reload();
   }
 }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
deleted file mode 100644
index a3afc5c..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-menu-page-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <style include="gr-page-nav-styles">
-    .breadcrumbText {
-      /* Same as dropdown trigger so chevron spacing is consistent. */
-      padding: 5px 4px;
-    }
-    iron-icon {
-      margin: 0 var(--spacing-xs);
-    }
-    .breadcrumb {
-      align-items: center;
-      display: flex;
-    }
-    .mainHeader {
-      align-items: baseline;
-      border-bottom: 1px solid var(--border-color);
-      display: flex;
-    }
-    .selectText {
-      display: none;
-    }
-    .selectText.show {
-      display: inline-block;
-    }
-    .main.breadcrumbs:not(.table) {
-      margin-top: var(--spacing-l);
-    }
-  </style>
-  <gr-page-nav class="navStyles">
-    <ul class="sectionContent">
-      <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
-        <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
-          <a class="title" href="[[_computeLinkURL(item)]]" rel="noopener"
-            >[[item.name]]</a
-          >
-        </li>
-        <template is="dom-repeat" items="[[item.children]]" as="child">
-          <li class$="[[_computeSelectedClass(child.view, params)]]">
-            <a href$="[[_computeLinkURL(child)]]" rel="noopener"
-              >[[child.name]]</a
-            >
-          </li>
-        </template>
-        <template is="dom-if" if="[[item.subsection]]">
-          <!--If a section has a subsection, render that.-->
-          <li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
-            <template is="dom-if" if="[[item.subsection.url]]" as="child">
-              <a
-                class="title"
-                href$="[[_computeLinkURL(item.subsection)]]"
-                rel="noopener"
-              >
-                [[item.subsection.name]]</a
-              >
-            </template>
-            <template is="dom-if" if="[[!item.subsection.url]]" as="child">
-              [[item.subsection.name]]
-            </template>
-          </li>
-          <!--Loop through the links in the sub-section.-->
-          <template
-            is="dom-repeat"
-            items="[[item.subsection.children]]"
-            as="child"
-          >
-            <li
-              class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]"
-            >
-              <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
-            </li>
-          </template>
-        </template>
-      </template>
-    </ul>
-  </gr-page-nav>
-  <template is="dom-if" if="[[_subsectionLinks.length]]">
-    <section class="mainHeader">
-      <span class="breadcrumb">
-        <span class="breadcrumbText">[[_breadcrumbParentName]]</span>
-        <iron-icon icon="gr-icons:chevron-right"></iron-icon>
-      </span>
-      <gr-dropdown-list
-        lowercase=""
-        id="pageSelect"
-        value="[[_computeSelectValue(params)]]"
-        items="[[_subsectionLinks]]"
-        on-value-change="_handleSubsectionChange"
-      >
-      </gr-dropdown-list>
-    </section>
-  </template>
-  <template is="dom-if" if="[[_showRepoList]]" restamp="true">
-    <div class="main table">
-      <gr-repo-list class="table" params="[[params]]"></gr-repo-list>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showGroupList]]" restamp="true">
-    <div class="main table">
-      <gr-admin-group-list class="table" params="[[params]]">
-      </gr-admin-group-list>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showPluginList]]" restamp="true">
-    <div class="main table">
-      <gr-plugin-list class="table" params="[[params]]"></gr-plugin-list>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showRepoMain]]" restamp="true">
-    <div class="main breadcrumbs">
-      <gr-repo repo="[[params.repo]]"></gr-repo>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showGroup]]" restamp="true">
-    <div class="main breadcrumbs">
-      <gr-group
-        group-id="[[params.groupId]]"
-        on-name-changed="_updateGroupName"
-      ></gr-group>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
-    <div class="main breadcrumbs">
-      <gr-group-members group-id="[[params.groupId]]"></gr-group-members>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
-    <div class="main table breadcrumbs">
-      <gr-repo-detail-list
-        params="[[params]]"
-        class="table"
-      ></gr-repo-detail-list>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
-    <div class="main table breadcrumbs">
-      <gr-group-audit-log
-        group-id="[[params.groupId]]"
-        class="table"
-      ></gr-group-audit-log>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
-    <div class="main breadcrumbs">
-      <gr-repo-commands repo="[[params.repo]]"></gr-repo-commands>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
-    <div class="main breadcrumbs">
-      <gr-repo-access path="[[path]]" repo="[[params.repo]]"></gr-repo-access>
-    </div>
-  </template>
-  <template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
-    <div class="main table breadcrumbs">
-      <gr-repo-dashboards repo="[[params.repo]]"></gr-repo-dashboards>
-    </div>
-  </template>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index 2c7ea82..5574ad1 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -47,33 +47,26 @@
     const pluginsLoaded = Promise.resolve();
     sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
     await pluginsLoaded;
-    await flush();
-  });
-
-  test('_computeURLHelper', () => {
-    const path = '/test';
-    const host = 'http://www.testsite.com';
-    const computedPath = element._computeURLHelper(host, path);
-    assert.equal(computedPath, '//http://www.testsite.com/test');
+    await element.updateComplete;
   });
 
   test('link URLs', () => {
     assert.equal(
-      element._computeLinkURL({name: '', url: '/test', noBaseUrl: true}),
+      element.computeLinkURL({name: '', url: '/test', noBaseUrl: true}),
       '//' + window.location.host + '/test'
     );
 
     stubBaseUrl('/foo');
     assert.equal(
-      element._computeLinkURL({name: '', url: '/test', noBaseUrl: true}),
+      element.computeLinkURL({name: '', url: '/test', noBaseUrl: true}),
       '//' + window.location.host + '/foo/test'
     );
     assert.equal(
-      element._computeLinkURL({name: '', url: '/test', noBaseUrl: false}),
+      element.computeLinkURL({name: '', url: '/test', noBaseUrl: false}),
       '/test'
     );
     assert.equal(
-      element._computeLinkURL({
+      element.computeLinkURL({
         name: '',
         url: '/test',
         target: '_blank',
@@ -83,8 +76,8 @@
     );
   });
 
-  test('current page gets selected and is displayed', () => {
-    element._filteredLinks = [
+  test('current page gets selected and is displayed', async () => {
+    element.filteredLinks = [
       {
         name: 'Repositories',
         url: '/admin/repos',
@@ -98,13 +91,13 @@
       adminView: 'gr-repo-list',
     };
 
-    flush();
+    await element.updateComplete;
     assert.equal(queryAll<HTMLLIElement>(element, '.selected').length, 1);
     assert.ok(queryAndAssert<GrRepoList>(element, 'gr-repo-list'));
     assert.isNotOk(query(element, 'gr-admin-create-repo'));
   });
 
-  test('_filteredLinks admin', async () => {
+  test('filteredLinks admin', async () => {
     stubRestApi('getAccount').returns(
       Promise.resolve({
         name: 'test-user',
@@ -115,36 +108,36 @@
       Promise.resolve(createAdminCapabilities())
     );
     await element.reload();
-    assert.equal(element._filteredLinks!.length, 3);
+    assert.equal(element.filteredLinks!.length, 3);
 
     // Repos
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
 
     // Groups
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
 
     // Plugins
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
   });
 
-  test('_filteredLinks non admin authenticated', async () => {
+  test('filteredLinks non admin authenticated', async () => {
     await element.reload();
-    assert.equal(element._filteredLinks!.length, 2);
+    assert.equal(element.filteredLinks!.length, 2);
     // Repos
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
     // Groups
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
   });
 
-  test('_filteredLinks non admin unathenticated', async () => {
+  test('filteredLinks non admin unathenticated', async () => {
     stubRestApi('getAccount').returns(Promise.resolve(undefined));
     await element.reload();
-    assert.equal(element._filteredLinks!.length, 1);
+    assert.equal(element.filteredLinks!.length, 1);
     // Repos
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
   });
 
-  test('_filteredLinks from plugin', () => {
+  test('filteredLinks from plugin', () => {
     stubRestApi('getAccount').returns(Promise.resolve(undefined));
     sinon.stub(element.jsAPI, 'getAdminMenuLinks').returns([
       {capability: null, text: 'internal link text', url: '/internal/link/url'},
@@ -155,8 +148,8 @@
       },
     ]);
     return element.reload().then(() => {
-      assert.equal(element._filteredLinks!.length, 3);
-      assert.deepEqual(element._filteredLinks![1], {
+      assert.equal(element.filteredLinks!.length, 3);
+      assert.deepEqual(element.filteredLinks![1], {
         capability: undefined,
         url: '/internal/link/url',
         name: 'internal link text',
@@ -165,7 +158,7 @@
         viewableToAll: true,
         target: null,
       });
-      assert.deepEqual(element._filteredLinks![2], {
+      assert.deepEqual(element.filteredLinks![2], {
         capability: undefined,
         url: 'http://external/link/url',
         name: 'external link text',
@@ -178,7 +171,7 @@
   });
 
   test('Repo shows up in nav', async () => {
-    element._repoName = 'Test Repo' as RepoName;
+    element.repoName = 'Test Repo' as RepoName;
     stubRestApi('getAccount').returns(
       Promise.resolve({
         name: 'test-user',
@@ -189,7 +182,7 @@
       Promise.resolve(createAdminCapabilities())
     );
     await element.reload();
-    await flush();
+    await element.updateComplete;
     assert.equal(queryAll<HTMLLIElement>(element, '.sectionTitle').length, 3);
     assert.equal(
       queryAndAssert<HTMLSpanElement>(element, '.breadcrumbText').innerText,
@@ -202,11 +195,10 @@
   });
 
   test('Group shows up in nav', async () => {
-    element._groupId = 'a15262' as GroupId;
-    element._groupName = 'my-group' as GroupName;
-    element._groupIsInternal = true;
-    element._isAdmin = true;
-    element._groupOwner = false;
+    element.groupId = 'a15262' as GroupId;
+    element.groupName = 'my-group' as GroupName;
+    element.groupIsInternal = true;
+    stubRestApi('getIsAdmin').returns(Promise.resolve(true));
     stubRestApi('getAccount').returns(
       Promise.resolve({
         name: 'test-user',
@@ -217,18 +209,18 @@
       Promise.resolve(createAdminCapabilities())
     );
     await element.reload();
-    await flush();
-    assert.equal(element._filteredLinks!.length, 3);
+    await element.updateComplete;
+    assert.equal(element.filteredLinks!.length, 3);
     // Repos
-    assert.isNotOk(element._filteredLinks![0].subsection);
+    assert.isNotOk(element.filteredLinks![0].subsection);
     // Groups
-    assert.equal(element._filteredLinks![1].subsection!.children!.length, 2);
-    assert.equal(element._filteredLinks![1].subsection!.name, 'my-group');
+    assert.equal(element.filteredLinks![1].subsection!.children!.length, 2);
+    assert.equal(element.filteredLinks![1].subsection!.name, 'my-group');
     // Plugins
-    assert.isNotOk(element._filteredLinks![2].subsection);
+    assert.isNotOk(element.filteredLinks![2].subsection);
   });
 
-  test('Nav is reloaded when repo changes', () => {
+  test('Nav is reloaded when repo changes', async () => {
     stubRestApi('getAccountCapabilities').returns(
       Promise.resolve(createAdminCapabilities())
     );
@@ -240,13 +232,15 @@
     );
     const reloadStub = sinon.stub(element, 'reload');
     element.params = {repo: 'Test Repo' as RepoName, view: GerritView.REPO};
+    await element.updateComplete;
     assert.equal(reloadStub.callCount, 1);
     element.params = {repo: 'Test Repo 2' as RepoName, view: GerritView.REPO};
+    await element.updateComplete;
     assert.equal(reloadStub.callCount, 2);
   });
 
-  test('Nav is reloaded when group changes', () => {
-    sinon.stub(element, '_computeGroupName');
+  test('Nav is reloaded when group changes', async () => {
+    sinon.stub(element, 'computeGroupName');
     stubRestApi('getAccountCapabilities').returns(
       Promise.resolve(createAdminCapabilities())
     );
@@ -258,20 +252,21 @@
     );
     const reloadStub = sinon.stub(element, 'reload');
     element.params = {groupId: '1' as GroupId, view: GerritView.GROUP};
+    await element.updateComplete;
     assert.equal(reloadStub.callCount, 1);
   });
 
   test('Nav is reloaded when group name changes', async () => {
     const newName = 'newName' as GroupName;
     const reloadCalled = mockPromise();
-    sinon.stub(element, '_computeGroupName');
+    sinon.stub(element, 'computeGroupName');
     sinon.stub(element, 'reload').callsFake(() => {
       reloadCalled.resolve();
       return Promise.resolve();
     });
     element.params = {groupId: '1' as GroupId, view: GerritView.GROUP};
-    element._groupName = 'oldName' as GroupName;
-    await flush();
+    element.groupName = 'oldName' as GroupName;
+    await element.updateComplete;
     queryAndAssert<GrGroup>(element, 'gr-group').dispatchEvent(
       new CustomEvent('name-changed', {
         detail: {name: newName},
@@ -280,12 +275,12 @@
       })
     );
     await reloadCalled;
-    assert.equal(element._groupName, newName);
+    assert.equal(element.groupName, newName);
   });
 
-  test('dropdown displays if there is a subsection', () => {
+  test('dropdown displays if there is a subsection', async () => {
     assert.isNotOk(query(element, '.mainHeader'));
-    element._subsectionLinks = [
+    element.subsectionLinks = [
       {
         text: 'Home',
         value: 'repo',
@@ -294,18 +289,15 @@
         detailType: undefined,
       },
     ];
-    flush();
+    await element.updateComplete;
     assert.isOk(query(element, '.mainHeader'));
-    element._subsectionLinks = undefined;
-    flush();
-    assert.equal(
-      getComputedStyle(queryAndAssert(element, '.mainHeader')).display,
-      'none'
-    );
+    element.subsectionLinks = undefined;
+    await element.updateComplete;
+    assert.isNotOk(query(element, '.mainHeader'));
   });
 
   test('Dropdown only triggers navigation on explicit select', async () => {
-    element._repoName = 'my-repo' as RepoName;
+    element.repoName = 'my-repo' as RepoName;
     element.params = {
       repo: 'my-repo' as RepoName,
       view: GerritNav.View.REPO,
@@ -320,7 +312,7 @@
         registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
       })
     );
-    await flush();
+    await element.updateComplete;
     const expectedFilteredLinks = [
       {
         name: 'Repositories',
@@ -451,12 +443,12 @@
     );
     const selectedIsCurrentPageSpy = sinon.spy(
       element,
-      '_selectedIsCurrentPage'
+      'selectedIsCurrentPage'
     );
-    sinon.spy(element, '_handleSubsectionChange');
+    sinon.spy(element, 'handleSubsectionChange');
     await element.reload();
-    assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
-    assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
+    assert.deepEqual(element.filteredLinks, expectedFilteredLinks);
+    assert.deepEqual(element.subsectionLinks, expectedSubsectionLinks);
     assert.equal(
       queryAndAssert<GrDropdownList>(element, '#pageSelect').value,
       'repoaccess'
@@ -472,8 +464,8 @@
     assert.isTrue(navigateToRelativeUrlStub.calledOnce);
   });
 
-  test('_selectedIsCurrentPage', () => {
-    element._repoName = 'my-repo' as RepoName;
+  test('selectedIsCurrentPage', () => {
+    element.repoName = 'my-repo' as RepoName;
     element.params = {view: GerritView.REPO, repo: 'my-repo' as RepoName};
     const selected = {
       view: GerritView.REPO,
@@ -481,15 +473,15 @@
       value: '',
       text: '',
     } as AdminSubsectionLink;
-    assert.isTrue(element._selectedIsCurrentPage(selected));
+    assert.isTrue(element.selectedIsCurrentPage(selected));
     selected.parent = 'my-second-repo' as RepoName;
-    assert.isFalse(element._selectedIsCurrentPage(selected));
+    assert.isFalse(element.selectedIsCurrentPage(selected));
     selected.detailType = GerritNav.RepoDetailView.GENERAL;
-    assert.isFalse(element._selectedIsCurrentPage(selected));
+    assert.isFalse(element.selectedIsCurrentPage(selected));
   });
 
-  suite('_computeSelectedClass', () => {
-    setup(() => {
+  suite('computeSelectedClass', () => {
+    setup(async () => {
       stubRestApi('getAccountCapabilities').returns(
         Promise.resolve(createAdminCapabilities())
       );
@@ -499,7 +491,7 @@
           registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
         })
       );
-      return element.reload();
+      await element.reload();
     });
 
     suite('repos', () => {
@@ -509,67 +501,64 @@
         );
       });
 
-      test('repo list', () => {
+      test('repo list', async () => {
         element.params = {
           view: GerritNav.View.ADMIN,
           adminView: 'gr-repo-list',
           openCreateModal: false,
         };
-        flush();
+        await element.updateComplete;
         const selected = queryAndAssert(element, 'gr-page-nav .selected');
         assert.isOk(selected);
         assert.equal(selected.textContent!.trim(), 'Repositories');
       });
 
-      test('repo', () => {
+      test('repo', async () => {
         element.params = {
           view: GerritNav.View.REPO,
           repo: 'foo' as RepoName,
         };
-        element._repoName = 'foo' as RepoName;
-        return element.reload().then(() => {
-          flush();
-          const selected = queryAndAssert(element, 'gr-page-nav .selected');
-          assert.isOk(selected);
-          assert.equal(selected.textContent!.trim(), 'foo');
-        });
+        element.repoName = 'foo' as RepoName;
+        await element.reload();
+        await element.updateComplete;
+        const selected = queryAndAssert(element, 'gr-page-nav .selected');
+        assert.isOk(selected);
+        assert.equal(selected.textContent!.trim(), 'foo');
       });
 
-      test('repo access', () => {
+      test('repo access', async () => {
         element.params = {
           view: GerritNav.View.REPO,
           detail: GerritNav.RepoDetailView.ACCESS,
           repo: 'foo' as RepoName,
         };
-        element._repoName = 'foo' as RepoName;
-        return element.reload().then(() => {
-          flush();
-          const selected = queryAndAssert(element, 'gr-page-nav .selected');
-          assert.isOk(selected);
-          assert.equal(selected.textContent!.trim(), 'Access');
-        });
+        element.repoName = 'foo' as RepoName;
+        await element.reload();
+        await element.updateComplete;
+        const selected = queryAndAssert(element, 'gr-page-nav .selected');
+        assert.isOk(selected);
+        assert.equal(selected.textContent!.trim(), 'Access');
       });
 
-      test('repo dashboards', () => {
+      test('repo dashboards', async () => {
         element.params = {
           view: GerritNav.View.REPO,
           detail: GerritNav.RepoDetailView.DASHBOARDS,
           repo: 'foo' as RepoName,
         };
-        element._repoName = 'foo' as RepoName;
-        return element.reload().then(() => {
-          flush();
-          const selected = queryAndAssert(element, 'gr-page-nav .selected');
-          assert.isOk(selected);
-          assert.equal(selected.textContent!.trim(), 'Dashboards');
-        });
+        element.repoName = 'foo' as RepoName;
+        await element.reload();
+        await element.updateComplete;
+        const selected = queryAndAssert(element, 'gr-page-nav .selected');
+        assert.isOk(selected);
+        assert.equal(selected.textContent!.trim(), 'Dashboards');
       });
     });
 
     suite('groups', () => {
       let getGroupConfigStub: sinon.SinonStub;
 
-      setup(() => {
+      setup(async () => {
         stub('gr-group', 'loadGroup').callsFake(() => Promise.resolve());
         stub('gr-group-members', 'loadGroupDetails').callsFake(() =>
           Promise.resolve()
@@ -583,42 +572,41 @@
           })
         );
         stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
-        return element.reload();
+        await element.reload();
       });
 
-      test('group list', () => {
+      test('group list', async () => {
         element.params = {
           view: GerritNav.View.ADMIN,
           adminView: 'gr-admin-group-list',
           openCreateModal: false,
         };
-        flush();
+        await element.updateComplete;
         const selected = queryAndAssert(element, 'gr-page-nav .selected');
         assert.isOk(selected);
         assert.equal(selected.textContent!.trim(), 'Groups');
       });
 
-      test('internal group', () => {
+      test('internal group', async () => {
         element.params = {
           view: GerritNav.View.GROUP,
           groupId: '1234' as GroupId,
         };
-        element._groupName = 'foo' as GroupName;
-        return element.reload().then(() => {
-          flush();
-          const subsectionItems = queryAll<HTMLLIElement>(
-            element,
-            '.subsectionItem'
-          );
-          assert.equal(subsectionItems.length, 2);
-          assert.isTrue(element._groupIsInternal);
-          const selected = queryAndAssert(element, 'gr-page-nav .selected');
-          assert.isOk(selected);
-          assert.equal(selected.textContent!.trim(), 'foo');
-        });
+        element.groupName = 'foo' as GroupName;
+        await element.reload();
+        await element.updateComplete;
+        const subsectionItems = queryAll<HTMLLIElement>(
+          element,
+          '.subsectionItem'
+        );
+        assert.equal(subsectionItems.length, 2);
+        assert.isTrue(element.groupIsInternal);
+        const selected = queryAndAssert(element, 'gr-page-nav .selected');
+        assert.isOk(selected);
+        assert.equal(selected.textContent!.trim(), 'foo');
       });
 
-      test('external group', () => {
+      test('external group', async () => {
         getGroupConfigStub.returns(
           Promise.resolve({
             name: 'foo',
@@ -629,34 +617,32 @@
           view: GerritNav.View.GROUP,
           groupId: '1234' as GroupId,
         };
-        element._groupName = 'foo' as GroupName;
-        return element.reload().then(() => {
-          flush();
-          const subsectionItems = queryAll<HTMLLIElement>(
-            element,
-            '.subsectionItem'
-          );
-          assert.equal(subsectionItems.length, 0);
-          assert.isFalse(element._groupIsInternal);
-          const selected = queryAndAssert(element, 'gr-page-nav .selected');
-          assert.isOk(selected);
-          assert.equal(selected.textContent!.trim(), 'foo');
-        });
+        element.groupName = 'foo' as GroupName;
+        await element.reload();
+        await element.updateComplete;
+        const subsectionItems = queryAll<HTMLLIElement>(
+          element,
+          '.subsectionItem'
+        );
+        assert.equal(subsectionItems.length, 0);
+        assert.isFalse(element.groupIsInternal);
+        const selected = queryAndAssert(element, 'gr-page-nav .selected');
+        assert.isOk(selected);
+        assert.equal(selected.textContent!.trim(), 'foo');
       });
 
-      test('group members', () => {
+      test('group members', async () => {
         element.params = {
           view: GerritNav.View.GROUP,
           detail: GerritNav.GroupDetailView.MEMBERS,
           groupId: '1234' as GroupId,
         };
-        element._groupName = 'foo' as GroupName;
-        return element.reload().then(() => {
-          flush();
-          const selected = queryAndAssert(element, 'gr-page-nav .selected');
-          assert.isOk(selected);
-          assert.equal(selected.textContent!.trim(), 'Members');
-        });
+        element.groupName = 'foo' as GroupName;
+        await element.reload();
+        await element.updateComplete;
+        const selected = queryAndAssert(element, 'gr-page-nav .selected');
+        assert.isOk(selected);
+        assert.equal(selected.textContent!.trim(), 'Members');
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index 0db155d..3393596 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -22,7 +22,7 @@
 import {ErrorCallback} from '../../../api/rest';
 import {encodeURL, getBaseUrl} from '../../../utils/url-util';
 import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
-import {ListViewParams} from '../../gr-app-types';
+import {AppElementAdminParams} from '../../gr-app-types';
 import {tableStyles} from '../../../styles/gr-table-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {LitElement, PropertyValues, css, html} from 'lit';
@@ -41,7 +41,7 @@
    * URL params passed from the router.
    */
   @property({type: Object})
-  params?: ListViewParams;
+  params?: AppElementAdminParams;
 
   /**
    * Offset of currently visible query results.
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts
index 221797c..f20b5a0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts
@@ -501,7 +501,8 @@
     });
   }
 
-  getIsGroupOwner(groupName: GroupName): Promise<boolean> {
+  getIsGroupOwner(groupName?: GroupName): Promise<boolean> {
+    if (!groupName) return Promise.resolve(false);
     const encodeName = encodeURIComponent(groupName);
     const req = {
       url: `/groups/?owned&g=${encodeName}`,
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index ba94929..7c35222 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -319,7 +319,7 @@
 
   getIsAdmin(): Promise<boolean | undefined>;
 
-  getIsGroupOwner(groupName: GroupName): Promise<boolean>;
+  getIsGroupOwner(groupName?: GroupName): Promise<boolean>;
 
   saveGroupName(
     groupId: GroupId | GroupName,
diff --git a/polygerrit-ui/app/utils/admin-nav-util.ts b/polygerrit-ui/app/utils/admin-nav-util.ts
index 86a3682..6688c19 100644
--- a/polygerrit-ui/app/utils/admin-nav-util.ts
+++ b/polygerrit-ui/app/utils/admin-nav-util.ts
@@ -258,4 +258,5 @@
   capability?: string;
   target?: string | null;
   subsection?: SubsectionInterface;
+  children?: SubsectionInterface[];
 }