Show summary for flows similar to checks

https://imgur.com/a/Sf4v6LM

Bug: Google b/431939712
Release-Notes: skip
Change-Id: Ia864ecfa1bfa838ba09bdd222f0e3e18a11823b2
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 017adca..591845a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import './gr-checks-chip';
+import './gr-summary-chip';
 import '../gr-comments-summary/gr-comments-summary';
 import '../../shared/gr-icon/gr-icon';
 import '../../checks/gr-checks-action';
@@ -29,6 +30,7 @@
 } from '../../../models/checks/checks-util';
 import {getMentionedThreads, isUnresolved} from '../../../utils/comment-util';
 import {AccountInfo, CommentThread, DropdownLink} from '../../../types/common';
+import {FlowInfo, FlowStageState} from '../../../api/rest-api';
 import {Tab} from '../../../constants/constants';
 import {ChecksTabState} from '../../../types/events';
 import {modifierPressed} from '../../../utils/dom-util';
@@ -43,6 +45,7 @@
 import {userModelToken} from '../../../models/user/user-model';
 import {assertIsDefined} from '../../../utils/common-util';
 import {GrAiPromptDialog} from '../gr-ai-prompt-dialog/gr-ai-prompt-dialog';
+import {flowsModelToken} from '../../../models/flows/flows-model';
 
 function handleSpaceOrEnter(e: KeyboardEvent, handler: () => void) {
   if (modifierPressed(e)) return;
@@ -98,6 +101,9 @@
   @state()
   draftCount = 0;
 
+  @state()
+  flows: FlowInfo[] = [];
+
   @query('#aiPromptModal')
   aiPromptModal?: HTMLDialogElement;
 
@@ -114,6 +120,8 @@
 
   private readonly getChangeModel = resolve(this, changeModelToken);
 
+  private readonly getFlowsModel = resolve(this, flowsModelToken);
+
   private readonly reporting = getAppContext().reportingService;
 
   constructor() {
@@ -189,6 +197,11 @@
         this.mentionCount = unresolvedThreadsMentioningSelf.length;
       }
     );
+    subscribe(
+      this,
+      () => this.getFlowsModel().flows$,
+      x => (this.flows = x)
+    );
   }
 
   static override get styles() {
@@ -584,7 +597,7 @@
               </div>
             </td>
           </tr>
-          ${this.renderChecksSummary()}
+          ${this.renderChecksSummary()} ${this.renderFlowsSummary()}
         </table>
       </div>
       <dialog id="aiPromptModal" tabindex="-1">
@@ -596,6 +609,81 @@
     `;
   }
 
+  private getFlowOverallStatus(flow: FlowInfo): FlowStageState {
+    if (
+      flow.stages.some(
+        stage =>
+          stage.state === FlowStageState.FAILED ||
+          stage.state === FlowStageState.TERMINATED
+      )
+    ) {
+      return FlowStageState.FAILED;
+    }
+    if (
+      flow.stages.some(
+        stage =>
+          stage.state === FlowStageState.PENDING ||
+          stage.state === FlowStageState.TERMINATED
+      )
+    ) {
+      return FlowStageState.PENDING; // Using PENDING to represent running/in-progress
+    }
+    if (flow.stages.every(stage => stage.state === FlowStageState.DONE)) {
+      return FlowStageState.DONE;
+    }
+    return FlowStageState.PENDING; // Default or unknown state
+  }
+
+  private renderFlowsSummary() {
+    if (this.flows.length === 0) return nothing;
+    const handler = () => fireShowTab(this, Tab.FLOWS, true);
+    const failed = this.flows.filter(
+      f => this.getFlowOverallStatus(f) === FlowStageState.FAILED
+    ).length;
+    const running = this.flows.filter(
+      f => this.getFlowOverallStatus(f) === FlowStageState.PENDING
+    ).length;
+    const done = this.flows.filter(
+      f => this.getFlowOverallStatus(f) === FlowStageState.DONE
+    ).length;
+    return html`
+      <tr>
+        <td class="key">Flows</td>
+        <td class="value">
+          <div class="flowsSummary">
+            ${failed > 0
+              ? html`<gr-checks-chip
+                  .statusOrCategory=${Category.ERROR}
+                  .text=${`${failed}`}
+                  @click=${handler}
+                  @keydown=${(e: KeyboardEvent) =>
+                    handleSpaceOrEnter(e, handler)}
+                ></gr-checks-chip>`
+              : ''}
+            ${running > 0
+              ? html`<gr-checks-chip
+                  .statusOrCategory=${RunStatus.RUNNING}
+                  .text=${`${running}`}
+                  @click=${handler}
+                  @keydown=${(e: KeyboardEvent) =>
+                    handleSpaceOrEnter(e, handler)}
+                ></gr-checks-chip>`
+              : ''}
+            ${done > 0
+              ? html`<gr-checks-chip
+                  .statusOrCategory=${Category.SUCCESS}
+                  .text=${`${done}`}
+                  @click=${handler}
+                  @keydown=${(e: KeyboardEvent) =>
+                    handleSpaceOrEnter(e, handler)}
+                ></gr-checks-chip>`
+              : ''}
+          </div>
+        </td>
+      </tr>
+    `;
+  }
+
   private renderChecksSummary() {
     const hasNonRunningChip = this.runs.some(
       run => hasCompletedWithoutResults(run) || hasResults(run)
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
index 2770ada..fd60d95 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary_test.ts
@@ -16,7 +16,7 @@
   createDraft,
   createRun,
 } from '../../../test/test-data-generators';
-import {Timestamp} from '../../../api/rest-api';
+import {FlowInfo, FlowStageState, Timestamp} from '../../../api/rest-api';
 import {testResolver} from '../../../test/common-test-setup';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {
@@ -26,16 +26,29 @@
 import {GrChecksChip} from './gr-checks-chip';
 import {CheckRun} from '../../../models/checks/checks-model';
 import {Category, RunStatus} from '../../../api/checks';
+import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
+
+function createFlow(partial: Partial<FlowInfo> = {}): FlowInfo {
+  return {
+    uuid: 'test-uuid',
+    owner: createAccountWithEmail(),
+    created: '2020-01-01 00:00:00.000000000' as Timestamp,
+    stages: [],
+    ...partial,
+  };
+}
 
 suite('gr-change-summary test', () => {
   let element: GrChangeSummary;
   let commentsModel: CommentsModel;
   let userModel: UserModel;
+  let flowsModel: FlowsModel;
 
   setup(async () => {
     element = await fixture(html`<gr-change-summary></gr-change-summary>`);
     commentsModel = testResolver(commentsModelToken);
     userModel = testResolver(userModelToken);
+    flowsModel = testResolver(flowsModelToken);
   });
 
   test('is defined', () => {
@@ -58,7 +71,8 @@
     await element.updateComplete;
     assert.shadowDom.equal(
       element,
-      /* HTML */ `<div>
+      /* HTML */ `
+        <div>
           <table class="info">
             <tbody>
               <tr>
@@ -86,7 +100,8 @@
         <dialog id="aiPromptModal" tabindex="-1">
           <gr-ai-prompt-dialog id="aiPromptDialog" role="dialog">
           </gr-ai-prompt-dialog>
-        </dialog> `
+        </dialog>
+      `
     );
   });
 
@@ -181,6 +196,62 @@
     });
   });
 
+  suite('flows summary', () => {
+    test('renders', async () => {
+      flowsModel.setState({
+        flows: [
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.PENDING},
+            ],
+          }),
+          createFlow({
+            stages: [{expression: {condition: ''}, state: FlowStageState.DONE}],
+          }),
+          createFlow({
+            stages: [{expression: {condition: ''}, state: FlowStageState.DONE}],
+          }),
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.FAILED},
+            ],
+          }),
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.FAILED},
+            ],
+          }),
+          createFlow({
+            stages: [
+              {expression: {condition: ''}, state: FlowStageState.FAILED},
+            ],
+          }),
+        ],
+        loading: false,
+      });
+      await element.updateComplete;
+      const flowsSummary = queryAndAssert(element, '.flowsSummary');
+      assert.dom.equal(
+        flowsSummary,
+        /* HTML */ `
+          <div class="flowsSummary">
+            <gr-checks-chip> </gr-checks-chip>
+            <gr-checks-chip> </gr-checks-chip>
+            <gr-checks-chip> </gr-checks-chip>
+          </div>
+        `
+      );
+      const chips = queryAll<GrChecksChip>(element, 'gr-checks-chip');
+      assert.equal(chips.length, 3);
+      assert.equal(chips[0].statusOrCategory, Category.ERROR);
+      assert.equal(chips[0].text, '3');
+      assert.equal(chips[1].statusOrCategory, RunStatus.RUNNING);
+      assert.equal(chips[1].text, '1');
+      assert.equal(chips[2].statusOrCategory, Category.SUCCESS);
+      assert.equal(chips[2].text, '2');
+    });
+  });
+
   test('renders mentions summary', async () => {
     commentsModel.setState({
       drafts: {