| /** |
| * @license |
| * Copyright (C) 2025 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 {ChangeInfo, NumericChangeId} from '@gerritcodereview/typescript-api/rest-api'; |
| import {LitElement, html, css, nothing} from 'lit'; |
| import {customElement, property, state} from 'lit/decorators.js'; |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-zuul': GrZuul; |
| } |
| } |
| |
| interface CrdInfo { |
| depends_on_found?: ChangeInfo[]; |
| depends_on_missing?: string[]; |
| needed_by?: ChangeInfo[]; |
| cycle?: boolean; |
| } |
| |
| // Partial copy of https://github.com/GerritCodeReview/gerrit/blob/b42341c5cd9b1f1535df30b16f180a90617fd067/polygerrit-ui/app/types/common.ts#L1377 |
| interface RelatedChangeAndCommitInfo { |
| _change_number?: NumericChangeId; |
| _revision_number?: number; |
| _current_revision_number?: number; |
| status?: string; |
| submittable?: boolean; |
| } |
| |
| |
| @customElement('gr-zuul') |
| export class GrZuul extends LitElement { |
| @property({type: Object}) change?: ChangeInfo; |
| |
| @state() private _crd: CrdInfo = {}; |
| @state() private _crdLoaded = false; |
| |
| static override get styles() { |
| return [ |
| css` |
| section.related-changes-section { |
| margin-bottom: 1.4em; |
| display: block; |
| } |
| a { |
| display: block; |
| max-width: 100%; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| .changeContainer { |
| display: flex; |
| max-width: 100%; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| .changeContainer.thisChange:before { |
| content: '➔'; |
| width: 1.2em; |
| } |
| h4, |
| section div { |
| display: flex; |
| } |
| h4:before, |
| section div:before { |
| content: ' '; |
| flex-shrink: 0; |
| width: 1.2em; |
| } |
| .status { |
| color: var(--deemphasized-text-color); |
| font-weight: var(--font-weight-bold); |
| margin-left: var(--spacing-xs); |
| } |
| .dependencyCycleDetected, |
| .missingFromThisServer { |
| color: #d17171; |
| } |
| .hidden { |
| display: none; |
| } |
| `, |
| ]; |
| } |
| |
| override render() { |
| if (!this._crdLoaded) return nothing; |
| |
| return html` |
| ${this._isDependsOnSectionVisible() |
| ? html` |
| <section class="related-changes-section"> |
| <h4>Depends on</h4> |
| ${this._crd.depends_on_found?.map( |
| item => html` |
| <div class="changeContainer zuulDependencyContainer"> |
| <a |
| href=${this._computeDependencyUrl(item)} |
| title="${item.project}: ${item.branch}: ${item.subject}" |
| > |
| ${item.project}: ${item.branch}: ${item.subject} |
| </a> |
| <span class=${this._computeChangeStatusClass(item)}> |
| (${this._computeChangeStatus(item)}) |
| </span> |
| ${this._crd.cycle |
| ? html` |
| <span class="status dependencyCycleDetected"> |
| (Dependency cycle detected) |
| </span> |
| ` |
| : nothing} |
| </div> |
| ` |
| )} |
| ${this._crd.depends_on_missing?.map( |
| item => html` |
| <div class="changeContainer zuulDependencyContainer"> |
| <span>${item}</span> |
| <span class="status missingFromThisServer"> |
| (Missing from this server) |
| </span> |
| </div> |
| ` |
| )} |
| </section> |
| ` |
| : nothing} |
| |
| ${this._crd.needed_by?.length |
| ? html` |
| <section class="related-changes-section"> |
| <h4>Needed by</h4> |
| ${this._crd.needed_by.map( |
| item => html` |
| <div class="changeContainer zuulDependencyContainer"> |
| <a |
| href=${this._computeDependencyUrl(item)} |
| title="${item.project}: ${item.branch}: ${item.subject}" |
| > |
| ${item.project}: ${item.branch}: ${item.subject} |
| </a> |
| <span class=${this._computeChangeStatusClass(item)}> |
| (${this._computeChangeStatus(item)}) |
| </span> |
| ${this._crd.cycle |
| ? html` |
| <span class="status dependencyCycleDetected"> |
| (Dependency cycle detected) |
| </span> |
| ` |
| : nothing} |
| </div> |
| ` |
| )} |
| </section> |
| ` |
| : nothing} |
| `; |
| } |
| |
| override updated(changedProperties: Map<string, unknown>) { |
| if (changedProperties.has('change')) { |
| void this._onChangeChanged(); |
| } |
| } |
| |
| private async _onChangeChanged(): Promise<void> { |
| this._crdLoaded = false; |
| this._setHidden(true); |
| if (!this.change?.id) return; |
| |
| const url = `/changes/${this.change.id}/revisions/current/crd`; |
| const plugin = (this as any).plugin; |
| |
| const crd: CrdInfo = await plugin.restApi().send('GET', url); |
| this._crd = crd; |
| this._crdLoaded = true; |
| |
| const visible = this._isDependsOnSectionVisible() || (crd.needed_by?.length ?? 0) > 0; |
| this._setHidden(!visible); |
| } |
| |
| private _setHidden(hidden: boolean): void { |
| if (this.hidden !== hidden) { |
| this.hidden = hidden; |
| this.dispatchEvent( |
| new CustomEvent('new-section-loaded', { |
| composed: true, |
| bubbles: true, |
| }) |
| ); |
| } |
| } |
| |
| private _computeChangeStatusClass(change: RelatedChangeAndCommitInfo): string { |
| const classes = ['status']; |
| if (change._revision_number !== change._current_revision_number) { |
| classes.push('notCurrent'); |
| } else if (change.submittable) { |
| classes.push('submittable'); |
| } else if (change.status === 'NEW') { |
| classes.push('hidden'); |
| } |
| return classes.join(' '); |
| } |
| |
| private _computeChangeStatus(change: RelatedChangeAndCommitInfo): string { |
| switch (change.status) { |
| case 'MERGED': |
| return 'Merged'; |
| case 'ABANDONED': |
| return 'Abandoned'; |
| default: |
| if (change._revision_number !== change._current_revision_number) { |
| return 'Not current'; |
| } else if (change.submittable) { |
| return 'Submittable'; |
| } |
| return ''; |
| } |
| } |
| |
| private _computeDependencyUrl(changeInfo: ChangeInfo): string { |
| const base = (window as any).CANONICAL_PATH || ''; |
| return `${base}/q/${changeInfo.change_id}`; |
| } |
| |
| private _isDependsOnSectionVisible(): boolean { |
| const {depends_on_found, depends_on_missing} = this._crd; |
| return (depends_on_found?.length ?? 0) + (depends_on_missing?.length ?? 0) > 0; |
| } |
| } |