|  | /** | 
|  | * @license | 
|  | * Copyright 2025 Google LLC | 
|  | * SPDX-License-Identifier: Apache-2.0 | 
|  | */ | 
|  | import {customElement, query, state} from 'lit/decorators.js'; | 
|  | import {css, html, LitElement, TemplateResult} from 'lit'; | 
|  | import {sharedStyles} from '../../../styles/shared-styles'; | 
|  | import {grFormStyles} from '../../../styles/gr-form-styles'; | 
|  | import {resolve} from '../../../models/dependency'; | 
|  | import {changeModelToken} from '../../../models/change/change-model'; | 
|  | import {subscribe} from '../../lit/subscription-controller'; | 
|  | import {FlowInfo, FlowStageState} from '../../../api/rest-api'; | 
|  | import {flowsModelToken} from '../../../models/flows/flows-model'; | 
|  | import {NumericChangeId} from '../../../types/common'; | 
|  | import './gr-create-flow'; | 
|  | import {when} from 'lit/directives/when.js'; | 
|  | import '../../shared/gr-dialog/gr-dialog'; | 
|  | import '@material/web/select/filled-select'; | 
|  | import '@material/web/select/select-option'; | 
|  |  | 
|  | const iconForFlowStageState = (status: FlowStageState) => { | 
|  | switch (status) { | 
|  | case FlowStageState.DONE: | 
|  | return {icon: 'check_circle', filled: true, class: 'done'}; | 
|  | case FlowStageState.PENDING: | 
|  | return {icon: 'timelapse', filled: false, class: 'pending'}; | 
|  | case FlowStageState.FAILED: | 
|  | return {icon: 'error', filled: true, class: 'failed'}; | 
|  | case FlowStageState.TERMINATED: | 
|  | return {icon: 'error', filled: true, class: 'failed'}; | 
|  | default: | 
|  | return {icon: 'help', filled: false, class: 'other'}; | 
|  | } | 
|  | }; | 
|  |  | 
|  | @customElement('gr-flows') | 
|  | export class GrFlows extends LitElement { | 
|  | @query('#deleteFlowModal') | 
|  | deleteFlowModal?: HTMLDialogElement; | 
|  |  | 
|  | @state() private flows: FlowInfo[] = []; | 
|  |  | 
|  | @state() private changeNum?: NumericChangeId; | 
|  |  | 
|  | @state() private loading = true; | 
|  |  | 
|  | @state() private flowIdToDelete?: string; | 
|  |  | 
|  | @state() private statusFilter: FlowStageState | 'all' = 'all'; | 
|  |  | 
|  | private readonly getChangeModel = resolve(this, changeModelToken); | 
|  |  | 
|  | private readonly getFlowsModel = resolve(this, flowsModelToken); | 
|  |  | 
|  | static override get styles() { | 
|  | return [ | 
|  | sharedStyles, | 
|  | grFormStyles, | 
|  | css` | 
|  | .container { | 
|  | padding: var(--spacing-l); | 
|  | } | 
|  | hr { | 
|  | margin-top: var(--spacing-l); | 
|  | margin-bottom: var(--spacing-l); | 
|  | border: 0; | 
|  | border-top: 1px solid var(--border-color); | 
|  | } | 
|  | .flow { | 
|  | border: 1px solid var(--border-color); | 
|  | border-radius: var(--border-radius); | 
|  | margin: var(--spacing-m) 0; | 
|  | padding: var(--spacing-m); | 
|  | } | 
|  | .flow-id { | 
|  | font-weight: var(--font-weight-bold); | 
|  | } | 
|  | .flow-header { | 
|  | display: flex; | 
|  | justify-content: space-between; | 
|  | align-items: center; | 
|  | margin-bottom: var(--spacing-s); | 
|  | } | 
|  | .heading-with-button { | 
|  | display: flex; | 
|  | align-items: center; | 
|  | } | 
|  | .hidden { | 
|  | display: none; | 
|  | } | 
|  | table { | 
|  | border-collapse: collapse; | 
|  | } | 
|  | th, | 
|  | td { | 
|  | border: 1px solid var(--border-color); | 
|  | padding: var(--spacing-s); | 
|  | text-align: left; | 
|  | } | 
|  | .main-heading { | 
|  | font-size: var(--font-size-h2); | 
|  | font-weight: var(--font-weight-bold); | 
|  | margin-bottom: var(--spacing-m); | 
|  | } | 
|  | gr-icon { | 
|  | font-size: var(--line-height-normal, 20px); | 
|  | vertical-align: middle; | 
|  | } | 
|  | gr-icon.done { | 
|  | color: var(--success-foreground); | 
|  | } | 
|  | gr-icon.pending { | 
|  | color: var(--deemphasized-text-color); | 
|  | } | 
|  | gr-icon.failed { | 
|  | color: var(--error-foreground); | 
|  | } | 
|  | .owner-container { | 
|  | display: flex; | 
|  | align-items: center; | 
|  | gap: var(--spacing-s); | 
|  | } | 
|  | .refresh { | 
|  | top: -4px; | 
|  | } | 
|  | `, | 
|  | ]; | 
|  | } | 
|  |  | 
|  | constructor() { | 
|  | super(); | 
|  | subscribe( | 
|  | this, | 
|  | () => this.getChangeModel().changeNum$, | 
|  | changeNum => { | 
|  | this.changeNum = changeNum; | 
|  | } | 
|  | ); | 
|  | subscribe( | 
|  | this, | 
|  | () => this.getFlowsModel().flows$, | 
|  | flows => { | 
|  | this.flows = flows; | 
|  | } | 
|  | ); | 
|  | subscribe( | 
|  | this, | 
|  | () => this.getFlowsModel().loading$, | 
|  | loading => { | 
|  | this.loading = loading; | 
|  | } | 
|  | ); | 
|  | } | 
|  |  | 
|  | private async deleteFlow() { | 
|  | if (!this.flowIdToDelete) return; | 
|  | await this.getFlowsModel().deleteFlow(this.flowIdToDelete); | 
|  | this.closeConfirmDialog(); | 
|  | } | 
|  |  | 
|  | private openConfirmDialog(flowId: string) { | 
|  | this.deleteFlowModal?.showModal(); | 
|  | this.flowIdToDelete = flowId; | 
|  | } | 
|  |  | 
|  | private closeConfirmDialog() { | 
|  | this.deleteFlowModal?.close(); | 
|  | this.flowIdToDelete = undefined; | 
|  | } | 
|  |  | 
|  | override render() { | 
|  | return html` | 
|  | <div class="container"> | 
|  | <h2 class="main-heading">Create new flow</h2> | 
|  | <gr-create-flow .changeNum=${this.changeNum}></gr-create-flow> | 
|  | <hr /> | 
|  | ${this.renderFlowsList()} | 
|  | </div> | 
|  | ${this.renderDeleteFlowModal()} | 
|  | `; | 
|  | } | 
|  |  | 
|  | private renderDeleteFlowModal() { | 
|  | return html` <dialog id="deleteFlowModal"> | 
|  | <gr-dialog | 
|  | confirm-label="Delete" | 
|  | @confirm=${() => this.deleteFlow()} | 
|  | @cancel=${() => this.closeConfirmDialog()} | 
|  | > | 
|  | <div class="header" slot="header">Delete Flow</div> | 
|  | <div class="main" slot="main"> | 
|  | Are you sure you want to delete this flow? | 
|  | </div> | 
|  | </gr-dialog> | 
|  | </dialog>`; | 
|  | } | 
|  |  | 
|  | private renderStatus(stage: FlowInfo['stages'][0]): TemplateResult { | 
|  | const icon = iconForFlowStageState(stage.state); | 
|  | return html`<gr-icon | 
|  | class=${icon.class} | 
|  | icon=${icon.icon} | 
|  | ?filled=${icon.filled} | 
|  | aria-label=${stage.state.toLowerCase()} | 
|  | role="img" | 
|  | ></gr-icon>`; | 
|  | } | 
|  |  | 
|  | private renderFlowsList() { | 
|  | if (this.loading) { | 
|  | return html`<p>Loading...</p>`; | 
|  | } | 
|  | if (this.flows.length === 0) { | 
|  | return html`<p>No flows found for this change.</p>`; | 
|  | } | 
|  | const filteredFlows = this.flows.filter(flow => { | 
|  | if (this.statusFilter === 'all') return true; | 
|  | const lastStage = flow.stages[flow.stages.length - 1]; | 
|  | return lastStage.state === this.statusFilter; | 
|  | }); | 
|  |  | 
|  | return html` | 
|  | <div> | 
|  | <div class="heading-with-button"> | 
|  | <h2 class="main-heading">Existing Flows</h2> | 
|  | <gr-button | 
|  | link | 
|  | @click=${() => this.getFlowsModel().reload()} | 
|  | aria-label="Refresh flows" | 
|  | title="Refresh flows" | 
|  | class="refresh" | 
|  | > | 
|  | <gr-icon icon="refresh"></gr-icon> | 
|  | </gr-button> | 
|  | </div> | 
|  | <md-filled-select | 
|  | label="Filter by status" | 
|  | @request-selection=${(e: CustomEvent) => { | 
|  | this.statusFilter = (e.target as HTMLSelectElement).value as | 
|  | | FlowStageState | 
|  | | 'all'; | 
|  | }} | 
|  | > | 
|  | <md-select-option value="all"> | 
|  | <div slot="headline">All</div> | 
|  | </md-select-option> | 
|  | ${Object.values(FlowStageState).map( | 
|  | status => html` | 
|  | <md-select-option value=${status}> | 
|  | <div slot="headline">${status}</div> | 
|  | </md-select-option> | 
|  | ` | 
|  | )} | 
|  | </md-filled-select> | 
|  | ${filteredFlows.map( | 
|  | (flow: FlowInfo) => html` | 
|  | <div class="flow"> | 
|  | <div class="flow-header"> | 
|  | <gr-button | 
|  | link | 
|  | @click=${() => this.openConfirmDialog(flow.uuid)} | 
|  | title="Delete flow" | 
|  | > | 
|  | <gr-icon icon="delete" filled></gr-icon> | 
|  | </gr-button> | 
|  | </div> | 
|  | <div class="flow-id hidden">Flow ${flow.uuid}</div> | 
|  | <div> | 
|  | Created: | 
|  | <gr-date-formatter withTooltip .dateStr=${flow.created}> | 
|  | </gr-date-formatter> | 
|  | </div> | 
|  | ${when( | 
|  | flow.last_evaluated, | 
|  | () => | 
|  | html` <div> | 
|  | Last Evaluated: | 
|  | <gr-date-formatter | 
|  | withTooltip | 
|  | .dateStr=${flow.last_evaluated} | 
|  | > | 
|  | </gr-date-formatter> | 
|  | </div>` | 
|  | )} | 
|  | <table> | 
|  | <thead> | 
|  | <tr> | 
|  | <th>Status</th> | 
|  | <th>Condition</th> | 
|  | <th>Action</th> | 
|  | <th>Parameters</th> | 
|  | <th>Message</th> | 
|  | </tr> | 
|  | </thead> | 
|  | <tbody> | 
|  | ${flow.stages.map(stage => { | 
|  | const action = stage.expression.action; | 
|  | return html` | 
|  | <tr> | 
|  | <td>${this.renderStatus(stage)}</td> | 
|  | <td>${stage.expression.condition}</td> | 
|  | <td>${action ? action.name : ''}</td> | 
|  | <td>${action ? action.parameters : ''}</td> | 
|  | <td>${stage.message ?? ''}</td> | 
|  | </tr> | 
|  | `; | 
|  | })} | 
|  | </tbody> | 
|  | </table> | 
|  | </div> | 
|  | ` | 
|  | )} | 
|  | </div> | 
|  | `; | 
|  | } | 
|  | } | 
|  |  | 
|  | declare global { | 
|  | interface HTMLElementTagNameMap { | 
|  | 'gr-flows': GrFlows; | 
|  | } | 
|  | } |