Merge changes from topic "checks-fetch"

* changes:
  Change the Checks fetch API to send a data object
  Add a plugin endpoint to the expanded check result
  Add support for top-level actions
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index 143fbd1..799d1f7 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -43,6 +43,12 @@
   fetchPollingIntervalSeconds: number;
 }
 
+export interface ChangeData {
+  changeNumber: number;
+  patchsetNumber: number;
+  repo: string;
+}
+
 export interface ChecksProvider {
   /**
    * Gerrit calls this method when ...
@@ -51,7 +57,7 @@
    * - ... while the tab is visible in a regular polling interval, see
    *       ChecksApiConfig.
    */
-  fetch(change: number, patchset: number): Promise<FetchResponse>;
+  fetch(change: ChangeData): Promise<FetchResponse>;
 }
 
 export interface FetchResponse {
@@ -224,10 +230,21 @@
 export type ActionCallback = (
   change: number,
   patchset: number,
+  /**
+   * Identical to 'attempt' property of CheckRun. Not set for top-level
+   * actions.
+   */
   attempt: number | undefined,
+  /**
+   * Identical to 'externalId' property of CheckRun. Not set for top-level
+   * actions.
+   */
   externalId: string | undefined,
-  /** Identical to 'checkName' property of CheckRun. */
-  checkName: string,
+  /**
+   * Identical to 'checkName' property of CheckRun. Not set for top-level
+   * actions.
+   */
+  checkName: string | undefined,
   /** Identical to 'name' property of Action entity. */
   actionName: string
 ) => Promise<ActionResult>;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 007af97..34945a9 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -81,7 +81,7 @@
         }
         td .summary-cell {
           display: flex;
-          max-width: calc(100vw - 579px);
+          max-width: calc(100vw - 700px);
         }
         td .summary-cell .summary {
           font-weight: var(--font-weight-bold);
@@ -237,9 +237,19 @@
   render() {
     if (!this.result) return '';
     return html`
-      <div class="message">
-        ${this.result.message}
-      </div>
+      <gr-endpoint-decorator name="check-result-expanded">
+        <gr-endpoint-param
+          name="run"
+          value="${this.result}"
+        ></gr-endpoint-param>
+        <gr-endpoint-param
+          name="result"
+          value="${this.result}"
+        ></gr-endpoint-param>
+        <div class="message">
+          ${this.result.message}
+        </div>
+      </gr-endpoint-decorator>
     `;
   }
 }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 4a34700..5b7428b 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -22,6 +22,7 @@
 import {sharedStyles} from '../../styles/shared-styles';
 import {
   compareByWorstCategory,
+  fireActionTriggered,
   iconForRun,
   primaryRunAction,
 } from '../../services/checks/checks-util';
@@ -60,33 +61,6 @@
   );
 }
 
-export interface ActionTriggeredEventDetail {
-  action: Action;
-  run: CheckRun;
-}
-
-export type ActionTriggeredEvent = CustomEvent<ActionTriggeredEventDetail>;
-
-declare global {
-  interface HTMLElementEventMap {
-    'action-triggered': ActionTriggeredEvent;
-  }
-}
-
-function fireActionTriggered(
-  target: EventTarget,
-  action: Action,
-  run: CheckRun
-) {
-  target.dispatchEvent(
-    new CustomEvent('action-triggered', {
-      detail: {action, run},
-      composed: true,
-      bubbles: true,
-    })
-  );
-}
-
 @customElement('gr-checks-run')
 export class GrChecksRun extends GrLitElement {
   static get styles() {
@@ -106,6 +80,11 @@
           margin-top: var(--spacing-s);
           cursor: pointer;
         }
+        .left {
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
         .name {
           font-weight: var(--font-weight-bold);
         }
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index d31ec53..1abe631 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -18,13 +18,20 @@
 import {css, customElement, property} from 'lit-element';
 import {GrLitElement} from '../lit/gr-lit-element';
 import {Action, CheckResult, CheckRun} from '../../api/checks';
-import {allResults$, allRuns$} from '../../services/checks/checks-model';
+import {
+  allActions$,
+  allResults$,
+  allRuns$,
+} from '../../services/checks/checks-model';
 import './gr-checks-runs';
 import './gr-checks-results';
 import {sharedStyles} from '../../styles/shared-styles';
 import {changeNum$, currentPatchNum$} from '../../services/change/change-model';
 import {NumericChangeId, PatchSetNum} from '../../types/common';
-import {ActionTriggeredEvent} from './gr-checks-runs';
+import {
+  ActionTriggeredEvent,
+  fireActionTriggered,
+} from '../../services/checks/checks-util';
 
 /**
  * The "Checks" tab on the Gerrit change page. Gets its data from plugins that
@@ -37,6 +44,8 @@
 
   results: CheckResult[] = [];
 
+  actions: Action[] = [];
+
   @property()
   currentPatchNum: PatchSetNum | undefined = undefined;
 
@@ -46,6 +55,7 @@
   constructor() {
     super();
     this.subscribe('runs', allRuns$);
+    this.subscribe('actions', allActions$);
     this.subscribe('results', allResults$);
     this.subscribe('currentPatchNum', currentPatchNum$);
     this.subscribe('changeNum', changeNum$);
@@ -63,17 +73,13 @@
           display: block;
         }
         .header {
-          display: block;
+          display: flex;
+          justify-content: space-between;
           padding: var(--spacing-m) var(--spacing-l);
           border-bottom: 1px solid var(--border-color);
         }
-        .header span {
-          display: inline-block;
-          color: var(--link-color);
-          padding: var(--spacing-s) var(--spacing-m);
-          margin-right: var(--spacing-l);
-          border: 1px solid var(--border-color);
-          border-radius: var(--border-radius);
+        .action {
+          margin-left: var(--spacing-m);
         }
         .container {
           display: flex;
@@ -95,15 +101,20 @@
     const ps = `Patchset ${this.currentPatchNum} (Latest)`;
     return html`
       <div class="header">
-        <gr-dropdown-list
-          value="${ps}"
-          .items="${[
-            {
-              value: `${ps}`,
-              text: `${ps}`,
-            },
-          ]}"
-        ></gr-dropdown-list>
+        <div class="left">
+          <gr-dropdown-list
+            value="${ps}"
+            .items="${[
+              {
+                value: `${ps}`,
+                text: `${ps}`,
+              },
+            ]}"
+          ></gr-dropdown-list>
+        </div>
+        <div class="right">
+          ${this.actions.map(this.renderAction)}
+        </div>
       </div>
       <div class="container">
         <gr-checks-runs class="runs" .runs="${this.runs}"></gr-checks-runs>
@@ -115,7 +126,13 @@
     `;
   }
 
-  private handleActionTriggered(action: Action, run: CheckRun) {
+  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.currentPatchNum) return;
     // TODO(brohlfs): The callback is supposed to be returning a promise.
@@ -124,16 +141,35 @@
     action.callback(
       this.changeNum,
       this.currentPatchNum as number,
-      run.attempt,
-      run.externalId,
-      run.checkName,
+      run?.attempt,
+      run?.externalId,
+      run?.checkName,
       action.name
     );
   }
 }
 
+@customElement('gr-checks-top-level-action')
+export class GrChecksTopLevelAction extends GrLitElement {
+  @property()
+  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/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 9546d0b..6705a85 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -17,6 +17,7 @@
 
 import {BehaviorSubject, Observable} from 'rxjs';
 import {
+  Action,
   Category,
   CheckResult,
   CheckRun,
@@ -35,6 +36,7 @@
   pluginName: string;
   config?: ChecksApiConfig;
   runs: CheckRun[];
+  actions: Action[];
 }
 
 interface ChecksState {
@@ -53,6 +55,18 @@
   distinctUntilChanged()
 );
 
+export const allActions$ = checksState$.pipe(
+  map(state => {
+    return Object.values(state).reduce(
+      (allActions: Action[], providerState: ChecksProviderState) => [
+        ...allActions,
+        ...providerState.actions,
+      ],
+      []
+    );
+  })
+);
+
 export const allRuns$ = checksState$.pipe(
   map(state => {
     return Object.values(state).reduce(
@@ -94,6 +108,7 @@
     pluginName,
     config,
     runs: [],
+    actions: [],
   };
   privateState$.next(nextState);
 }
@@ -162,11 +177,16 @@
   status: RunStatus.COMPLETED,
 };
 
-export function updateStateSetResults(pluginName: string, runs: CheckRun[]) {
+export function updateStateSetResults(
+  pluginName: string,
+  runs: CheckRun[],
+  actions: Action[] = []
+) {
   const nextState = {...privateState$.getValue()};
   nextState[pluginName] = {
     ...nextState[pluginName],
     runs: [...runs],
+    actions: [...actions],
   };
   privateState$.next(nextState);
 }
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
index a8dd8b8..2e63f98 100644
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ b/polygerrit-ui/app/services/checks/checks-service.ts
@@ -23,6 +23,7 @@
   withLatestFrom,
 } from 'rxjs/operators';
 import {
+  ChangeData,
   ChecksApiConfig,
   ChecksProvider,
   FetchResponse,
@@ -77,13 +78,20 @@
                 runs: [],
               });
             }
-            return from(
-              this.providers[pluginName].fetch(change._number, patchNum)
-            );
+            const data: ChangeData = {
+              changeNumber: change._number,
+              patchsetNumber: patchNum,
+              repo: change.project,
+            };
+            return from(this.providers[pluginName].fetch(data));
           }
         ),
         tap(response => {
-          updateStateSetResults(pluginName, response.runs ?? []);
+          updateStateSetResults(
+            pluginName,
+            response.runs ?? [],
+            response.actions
+          );
         })
       )
       .subscribe(() => {});
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 30b82b6..176464f 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -135,3 +135,30 @@
       return 2;
   }
 }
+
+export interface ActionTriggeredEventDetail {
+  action: Action;
+  run?: CheckRun;
+}
+
+export type ActionTriggeredEvent = CustomEvent<ActionTriggeredEventDetail>;
+
+declare global {
+  interface HTMLElementEventMap {
+    'action-triggered': ActionTriggeredEvent;
+  }
+}
+
+export function fireActionTriggered(
+  target: EventTarget,
+  action: Action,
+  run?: CheckRun
+) {
+  target.dispatchEvent(
+    new CustomEvent('action-triggered', {
+      detail: {action, run},
+      composed: true,
+      bubbles: true,
+    })
+  );
+}