Update the header of the Checks tab

Screenshot https://imgur.com/a/f4PX1Tt

Change-Id: I3d33f6a316b971135b3ce6fd79ae41ed413c707c
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 197e11b..804d85f 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -26,21 +26,42 @@
 } from 'lit-element';
 import {GrLitElement} from '../lit/gr-lit-element';
 import '@polymer/paper-tooltip/paper-tooltip';
-import {Category, Link, LinkIcon, RunStatus, Tag} from '../../api/checks';
+import {
+  Action,
+  Category,
+  Link,
+  LinkIcon,
+  RunStatus,
+  Tag,
+} from '../../api/checks';
 import {sharedStyles} from '../../styles/shared-styles';
-import {CheckRun, RunResult} from '../../services/checks/checks-model';
+import {
+  allActions$,
+  checksPatchsetNumber$,
+  CheckRun,
+  RunResult,
+  someProvidersAreLoading$,
+} from '../../services/checks/checks-model';
 import {
   allResults,
+  fireActionTriggered,
   hasCompletedWithoutResults,
   hasResultsOf,
   iconForCategory,
 } from '../../services/checks/checks-util';
-import {assertIsDefined} from '../../utils/common-util';
+import {
+  assertIsDefined,
+  check,
+  checkRequiredProperty,
+} from '../../utils/common-util';
 import {whenVisible} from '../../utils/dom-util';
 import {durationString} from '../../utils/date-util';
 import {charsOnly, pluralize} from '../../utils/string-util';
 import {fireRunSelectionReset} from './gr-checks-util';
 import {ChecksTabState} from '../../types/events';
+import {PatchSetNumber} from '../../types/common';
+import {latestPatchNum$} from '../../services/change/change-model';
+import {appContext} from '../../services/app-context';
 
 @customElement('gr-result-row')
 class GrResultRow extends GrLitElement {
@@ -330,8 +351,20 @@
   runs: CheckRun[] = [];
 
   @property()
+  actions: Action[] = [];
+
+  @property()
   tabState?: ChecksTabState;
 
+  @property()
+  someProvidersAreLoading = false;
+
+  @property()
+  checksPatchsetNumber: PatchSetNumber | undefined = undefined;
+
+  @property()
+  latestPatchsetNumber: PatchSetNumber | undefined = undefined;
+
   /**
    * How many runs are selected in the runs panel?
    * If 0, then the `runs` property contains all the runs there are.
@@ -354,13 +387,59 @@
    */
   private isSectionExpandedByUser = new Map<Category | 'SUCCESS', boolean>();
 
+  private readonly checksService = appContext.checksService;
+
+  constructor() {
+    super();
+    this.subscribe('actions', allActions$);
+    this.subscribe('checksPatchsetNumber', checksPatchsetNumber$);
+    this.subscribe('latestPatchsetNumber', latestPatchNum$);
+    this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
+  }
+
   static get styles() {
     return [
       sharedStyles,
       css`
         :host {
           display: block;
-          padding: var(--spacing-xl);
+          background-color: var(--background-color-secondary);
+        }
+        .header {
+          display: block;
+          background-color: var(--background-color-primary);
+          padding: var(--spacing-l) var(--spacing-xl) var(--spacing-m)
+            var(--spacing-xl);
+          border-bottom: 1px solid var(--border-color);
+        }
+        .headerTopRow,
+        .headerBottomRow {
+          display: flex;
+          justify-content: space-between;
+          align-items: flex-end;
+        }
+        .headerTopRow gr-dropdown-list {
+          border: 1px solid var(--border-color);
+          border-radius: var(--border-radius);
+          padding: 0 var(--spacing-m);
+        }
+        .headerBottomRow {
+          margin-top: var(--spacing-s);
+        }
+        .headerBottomRow .right {
+          display: flex;
+          align-items: center;
+        }
+        #moreActions iron-icon {
+          color: var(--link-color);
+        }
+        #moreMessage {
+          display: none;
+        }
+        .body {
+          display: block;
+          padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl)
+            var(--spacing-xl);
         }
         .filterDiv {
           display: flex;
@@ -463,7 +542,7 @@
     }
   }
 
-  scrollElIntoView(selector: string) {
+  private scrollElIntoView(selector: string) {
     this.updateComplete.then(() => {
       let el = this.shadowRoot?.querySelector(selector);
       // <gr-result-row> has display:contents and cannot be scrolled into view
@@ -475,13 +554,92 @@
 
   render() {
     return html`
-      <div><h2 class="heading-2">Results</h2></div>
-      ${this.renderFilter()} ${this.renderSection(Category.ERROR)}
-      ${this.renderSection(Category.WARNING)}
-      ${this.renderSection(Category.INFO)} ${this.renderSection('SUCCESS')}
+      <div class="header">
+        <div class="headerTopRow">
+          <div class="left">
+            <h2 class="heading-2">Results</h2>
+          </div>
+          <div class="middle">
+            <span ?hidden="${!this.someProvidersAreLoading}">Loading...</span>
+          </div>
+          <div class="right">
+            <gr-dropdown-list
+              value="${this.checksPatchsetNumber}"
+              .items="${this.createPatchsetDropdownItems()}"
+              @value-change="${this.onPatchsetSelected}"
+            ></gr-dropdown-list>
+          </div>
+        </div>
+        <div class="headerBottomRow">
+          <div class="left">
+            ${this.renderFilter()}
+          </div>
+          <div class="right">
+            ${this.renderActions()}
+          </div>
+        </div>
+      </div>
+      <div class="body">
+        ${this.renderSection(Category.ERROR)}
+        ${this.renderSection(Category.WARNING)}
+        ${this.renderSection(Category.INFO)} ${this.renderSection('SUCCESS')}
+      </div>
     `;
   }
 
+  private renderActions() {
+    const overflowItems = this.actions.slice(2).map(action => {
+      return {...action, id: action.name};
+    });
+    return html`
+      ${this.renderAction(this.actions[0])}
+      ${this.renderAction(this.actions[1])}
+      <gr-dropdown
+        id="moreActions"
+        link=""
+        vertical-offset="32"
+        horizontal-align="right"
+        @tap-item="${this.handleAction}"
+        ?hidden="${overflowItems.length === 0}"
+        .items="${overflowItems}"
+      >
+        <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
+        </iron-icon>
+        <span id="moreMessage">More</span>
+      </gr-dropdown>
+    `;
+  }
+
+  private handleAction(e: CustomEvent<Action>) {
+    fireActionTriggered(this, e.detail);
+  }
+
+  private renderAction(action?: Action) {
+    if (!action) return;
+    return html`<gr-checks-top-level-action
+      .action="${action}"
+    ></gr-checks-top-level-action>`;
+  }
+
+  private onPatchsetSelected(e: CustomEvent<{value: string}>) {
+    const patchset = Number(e.detail.value);
+    check(!isNaN(patchset), 'selected patchset must be a number');
+    this.checksService.setPatchset(patchset as PatchSetNumber);
+  }
+
+  private createPatchsetDropdownItems() {
+    if (!this.latestPatchsetNumber) return [];
+    return Array.from(Array(this.latestPatchsetNumber), (_, i) => {
+      assertIsDefined(this.latestPatchsetNumber, 'latestPatchsetNumber');
+      const index = this.latestPatchsetNumber - i;
+      const postfix = index === this.latestPatchsetNumber ? ' (latest)' : '';
+      return {
+        value: `${index}`,
+        text: `Patchset ${index}${postfix}`,
+      };
+    });
+  }
+
   renderFilter() {
     if (this.selectedRunsCount === 0 && allResults(this.runs).length <= 3) {
       if (this.filterRegExp.source.length > 0) {
@@ -667,10 +825,50 @@
   }
 }
 
+@customElement('gr-checks-top-level-action')
+export class GrChecksTopLevelAction extends GrLitElement {
+  @property()
+  action!: Action;
+
+  connectedCallback() {
+    super.connectedCallback();
+    checkRequiredProperty(this.action, 'action');
+  }
+
+  static get styles() {
+    return [
+      css`
+        gr-button {
+          --padding: var(--spacing-s) var(--spacing-m);
+        }
+        gr-button paper-tooltip {
+          text-transform: none;
+        }
+      `,
+    ];
+  }
+
+  render() {
+    return html`
+      <gr-button link class="action" @click="${this.handleClick}">
+        ${this.action.name}
+        <paper-tooltip ?hidden="${!this.action.tooltip}" offset="5"
+          >${this.action.tooltip}</paper-tooltip
+        >
+      </gr-button>
+    `;
+  }
+
+  handleClick() {
+    fireActionTriggered(this, this.action);
+  }
+}
+
 declare global {
   interface HTMLElementTagNameMap {
     'gr-result-row': GrResultRow;
     'gr-result-expanded': GrResultExpanded;
     'gr-checks-results': GrChecksResults;
+    'gr-checks-top-level-action': GrChecksTopLevelAction;
   }
 }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index c531874..3b07cf2 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -38,6 +38,7 @@
 import {
   CheckRun,
   allRuns$,
+  fakeActions,
   fakeRun0,
   fakeRun1,
   fakeRun2,
@@ -258,7 +259,8 @@
       css`
         :host {
           display: block;
-          padding: var(--spacing-xl);
+          padding: var(--spacing-l) var(--spacing-xl) var(--spacing-xl)
+            var(--spacing-xl);
         }
         .expandIcon {
           width: var(--line-height-h3);
@@ -280,7 +282,7 @@
           padding-bottom: var(--spacing-m);
         }
         input#filterInput {
-          margin-top: var(--spacing-s);
+          margin-top: var(--spacing-m);
           padding: var(--spacing-s) var(--spacing-m);
           width: 100%;
         }
@@ -334,7 +336,9 @@
       <div class="testing">
         <div>Toggle fake runs by clicking buttons:</div>
         <gr-button link @click="${this.none}">none</gr-button>
-        <gr-button link @click="${() => this.toggle('f0', fakeRun0)}"
+        <gr-button
+          link
+          @click="${() => this.toggle('f0', fakeRun0, fakeActions)}"
           >0</gr-button
         >
         <gr-button link @click="${() => this.toggle('f1', fakeRun1)}"
@@ -360,7 +364,7 @@
   }
 
   none() {
-    updateStateSetResults('f0', []);
+    updateStateSetResults('f0', [], []);
     updateStateSetResults('f1', []);
     updateStateSetResults('f2', []);
     updateStateSetResults('f3', []);
@@ -368,16 +372,16 @@
   }
 
   all() {
-    updateStateSetResults('f0', [fakeRun0]);
+    updateStateSetResults('f0', [fakeRun0], fakeActions);
     updateStateSetResults('f1', [fakeRun1]);
     updateStateSetResults('f2', [fakeRun2]);
     updateStateSetResults('f3', [fakeRun3]);
     updateStateSetResults('f4', [fakeRun4]);
   }
 
-  toggle(plugin: string, run: CheckRun) {
+  toggle(plugin: string, run: CheckRun, actions: Action[] = []) {
     const newRuns = this.runs.includes(run) ? [] : [run];
-    updateStateSetResults(plugin, newRuns);
+    updateStateSetResults(plugin, newRuns, actions);
   }
 
   renderSection(status: RunStatus) {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 6e11265..d017eb7 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -27,26 +27,15 @@
 import {
   CheckResult,
   CheckRun,
-  allActions$,
   allResults$,
   allRuns$,
   checksPatchsetNumber$,
-  someProvidersAreLoading$,
 } from '../../services/checks/checks-model';
 import './gr-checks-runs';
 import './gr-checks-results';
-import {sharedStyles} from '../../styles/shared-styles';
-import {changeNum$, latestPatchNum$} from '../../services/change/change-model';
+import {changeNum$} from '../../services/change/change-model';
 import {NumericChangeId, PatchSetNumber} from '../../types/common';
-import {
-  ActionTriggeredEvent,
-  fireActionTriggered,
-} from '../../services/checks/checks-util';
-import {
-  assertIsDefined,
-  check,
-  checkRequiredProperty,
-} from '../../utils/common-util';
+import {ActionTriggeredEvent} from '../../services/checks/checks-util';
 import {RunSelectedEvent} from './gr-checks-util';
 import {ChecksTabState} from '../../types/events';
 import {fireAlert} from '../../utils/event-util';
@@ -63,10 +52,9 @@
   @property()
   runs: CheckRun[] = [];
 
+  @property()
   results: CheckResult[] = [];
 
-  actions: Action[] = [];
-
   @property()
   tabState?: ChecksTabState;
 
@@ -74,14 +62,8 @@
   checksPatchsetNumber: PatchSetNumber | undefined = undefined;
 
   @property()
-  latestPatchsetNumber: PatchSetNumber | undefined = undefined;
-
-  @property()
   changeNum: NumericChangeId | undefined = undefined;
 
-  @property()
-  someProvidersAreLoading = false;
-
   @internalProperty()
   selectedRuns: string[] = [];
 
@@ -90,12 +72,9 @@
   constructor() {
     super();
     this.subscribe('runs', allRuns$);
-    this.subscribe('actions', allActions$);
     this.subscribe('results', allResults$);
     this.subscribe('checksPatchsetNumber', checksPatchsetNumber$);
-    this.subscribe('latestPatchsetNumber', latestPatchNum$);
     this.subscribe('changeNum', changeNum$);
-    this.subscribe('someProvidersAreLoading', someProvidersAreLoading$);
 
     this.addEventListener('action-triggered', (e: ActionTriggeredEvent) =>
       this.handleActionTriggered(e.detail.action, e.detail.run)
@@ -103,35 +82,22 @@
   }
 
   static get styles() {
-    return [
-      sharedStyles,
-      css`
-        :host {
-          display: block;
-        }
-        .header {
-          display: flex;
-          justify-content: space-between;
-          padding: var(--spacing-m) var(--spacing-l);
-          border-bottom: 1px solid var(--border-color);
-        }
-        .action {
-          margin-left: var(--spacing-m);
-        }
-        .container {
-          display: flex;
-        }
-        .runs {
-          min-width: 300px;
-          min-height: 400px;
-          border-right: 1px solid var(--border-color);
-        }
-        .results {
-          background-color: var(--background-color-secondary);
-          flex-grow: 1;
-        }
-      `,
-    ];
+    return css`
+      :host {
+        display: block;
+      }
+      .container {
+        display: flex;
+      }
+      .runs {
+        min-width: 300px;
+        min-height: 400px;
+        border-right: 1px solid var(--border-color);
+      }
+      .results {
+        flex-grow: 1;
+      }
+    `;
   }
 
   render() {
@@ -141,19 +107,6 @@
         this.selectedRuns.includes(r.checkName)
     );
     return html`
-      <div class="header">
-        <div class="left">
-          <gr-dropdown-list
-            value="${this.checksPatchsetNumber}"
-            .items="${this.createPatchsetDropdownItems()}"
-            @value-change="${this.onPatchsetSelected}"
-          ></gr-dropdown-list>
-          <span ?hidden="${!this.someProvidersAreLoading}">Loading...</span>
-        </div>
-        <div class="right">
-          ${this.actions.map(this.renderAction)}
-        </div>
-      </div>
       <div class="container">
         <gr-checks-runs
           class="runs"
@@ -173,25 +126,6 @@
     `;
   }
 
-  private onPatchsetSelected(e: CustomEvent<{value: string}>) {
-    const patchset = Number(e.detail.value);
-    check(!isNaN(patchset), 'selected patchset must be a number');
-    this.checksService.setPatchset(patchset as PatchSetNumber);
-  }
-
-  private createPatchsetDropdownItems() {
-    if (!this.latestPatchsetNumber) return [];
-    return Array.from(Array(this.latestPatchsetNumber), (_, i) => {
-      assertIsDefined(this.latestPatchsetNumber, 'latestPatchsetNumber');
-      const index = this.latestPatchsetNumber - i;
-      const postfix = index === this.latestPatchsetNumber ? ' (latest)' : '';
-      return {
-        value: `${index}`,
-        text: `Patchset ${index}${postfix}`,
-      };
-    });
-  }
-
   protected updated(changedProperties: PropertyValues) {
     super.updated(changedProperties);
     if (changedProperties.has('tabState')) {
@@ -201,12 +135,6 @@
     }
   }
 
-  renderAction(action: Action) {
-    return html`<gr-checks-top-level-action
-      .action="${action}"
-    ></gr-checks-top-level-action>`;
-  }
-
   handleActionTriggered(action: Action, run?: CheckRun) {
     if (!this.changeNum) return;
     if (!this.checksPatchsetNumber) return;
@@ -258,32 +186,8 @@
   }
 }
 
-@customElement('gr-checks-top-level-action')
-export class GrChecksTopLevelAction extends GrLitElement {
-  @property()
-  action!: Action;
-
-  connectedCallback() {
-    super.connectedCallback();
-    checkRequiredProperty(this.action, 'action');
-  }
-
-  render() {
-    return html`
-      <gr-button link class="action" @click="${this.handleClick}"
-        >${this.action.name}</gr-button
-      >
-    `;
-  }
-
-  handleClick() {
-    fireActionTriggered(this, this.action);
-  }
-}
-
 declare global {
   interface HTMLElementTagNameMap {
     'gr-checks-tab': GrChecksTab;
-    'gr-checks-top-level-action': GrChecksTopLevelAction;
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
index c767edd..b1fa7bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
@@ -30,6 +30,7 @@
       box-shadow: var(--elevation-level-2);
     }
     gr-button {
+      vertical-align: top;
       @apply --gr-button;
     }
     gr-avatar {
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 302d0f1..81478bf 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -241,6 +241,36 @@
   status: RunStatus.COMPLETED,
 };
 
+export const fakeActions: Action[] = [
+  {
+    name: 'Fake Action 1',
+    primary: false,
+    tooltip: 'Tooltip for Fake Action 1',
+    callback: () => {
+      console.warn('fake action 1 triggered');
+      return undefined;
+    },
+  },
+  {
+    name: 'Fake Action 2',
+    primary: false,
+    tooltip: 'Tooltip for Fake Action 2',
+    callback: () => {
+      console.warn('fake action 2 triggered');
+      return undefined;
+    },
+  },
+  {
+    name: 'Fake Action 3',
+    primary: false,
+    tooltip: 'Tooltip for Fake Action 3',
+    callback: () => {
+      console.warn('fake action 3 triggered');
+      return undefined;
+    },
+  },
+];
+
 export function updateStateSetLoading(pluginName: string) {
   const nextState = {...privateState$.getValue()};
   nextState.providerNameToState = {...nextState.providerNameToState};