Repository: ls1intum/Helios
Pull request #540: feat: link pr in the env view
Author: login: gbanu
Labels: name: size:L
name: client
name: ready for review
name: application-server
name: feature
Created: 2025-03-15T16:26:29Z | Merged: 2025-03-17T22:09:11Z

I didn't want to overload the environment list component even more that's why added a link to the existing latest deployment tag.

https://github.com/user-attachments/assets/eeac0b13-fa36-4bda-a916-d17f2edcd72f

fixes https://github.com/ls1intum/Helios/issues/317



Diff:
diff --git a/client/src/app/components/environments/environment-list/environment-list-view.component.html b/client/src/app/components/environments/environment-list/environment-list-view.component.html
index 07956992a..c8e8034d1 100644
--- a/client/src/app/components/environments/environment-list/environment-list-view.component.html
+++ b/client/src/app/components/environments/environment-list/environment-list-view.component.html
@@ -44,13 +44,39 @@
                   <span [pTooltip]="getDeploymentTime(environment) || ''">{{ environment.latestDeployment.updatedAt || '' | timeAgo }}</span>
                 }
                 <app-deployment-state-tag [state]="deployment.state" />
-                <p-tag severity="secondary" rounded="true" class="max-w-[350px] flex items-center" [pTooltip]="tooltipTemplate" tooltipPosition="top">
-                  <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+
+                <!-- Branch or Release Tag -->
+                <p-tag
+                  severity="secondary"
+                  rounded="true"
+                  class="max-w-[350px] flex items-center"
+                  [pTooltip]="isRelease(deployment) ? '' : 'Go to ' + deployment.ref"
+                  tooltipPosition="top"
+                  (click)="$event.stopPropagation()"
+                  [routerLink]="isRelease(deployment) ? null : getBranchLink(environment)"
+                >
+                  @if (isRelease(deployment)) {
+                    <i-tabler name="tag" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                  } @else {
+                    <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                  }
                   <span class="ml-1 truncate flex-1">{{ deployment.ref }}</span>
-                  <ng-template #tooltipTemplate>
-                    <div class="flex items-center">{{ deployment.ref }}</div>
-                  </ng-template>
                 </p-tag>
+                @if (deployment.pullRequestNumber) {
+                  <!-- PR Tag -->
+                  <p-tag
+                    severity="secondary"
+                    rounded="true"
+                    class="max-w-[350px] flex items-center"
+                    [pTooltip]="'Go to ' + (deployment.prName || 'Pull Request #' + deployment.pullRequestNumber)"
+                    tooltipPosition="top"
+                    (click)="$event.stopPropagation()"
+                    [routerLink]="getPrLink(environment)"
+                  >
+                    <i-tabler name="git-pull-request" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                    <span class="ml-1 truncate flex-1">#{{ deployment.pullRequestNumber }}</span>
+                  </p-tag>
+                }
               </div>
             }
           </div>
diff --git a/client/src/app/components/environments/environment-list/environment-list-view.component.ts b/client/src/app/components/environments/environment-list/environment-list-view.component.ts
index 2d535869c..25be44b0a 100644
--- a/client/src/app/components/environments/environment-list/environment-list-view.component.ts
+++ b/client/src/app/components/environments/environment-list/environment-list-view.component.ts
@@ -3,7 +3,7 @@ import { Component, computed, inject, input, OnDestroy, output, signal } from '@
 import { FormsModule } from '@angular/forms';
 import { RouterLink } from '@angular/router';
 import { UserAvatarComponent } from '@app/components/user-avatar/user-avatar.component';
-import { EnvironmentDto } from '@app/core/modules/openapi';
+import { EnvironmentDeployment, EnvironmentDto } from '@app/core/modules/openapi';
 import {
   extendEnvironmentLockMutation,
   getAllEnabledEnvironmentsOptions,
@@ -303,4 +303,18 @@ export class EnvironmentListViewComponent implements OnDestroy {
     }
     return true;
   }
+
+  isRelease(deployment: EnvironmentDeployment): boolean {
+    // TODO: This is a temporary solution to check if a deployment is a release
+    // until Paul's PR is merged which enables syncing of releases from GitHub to find a corresponding release
+    return !!deployment.releaseCandidateName || (!!deployment.ref && /^v?\d+\.\d+\.\d+/.test(deployment.ref));
+  }
+
+  getPrLink(env: EnvironmentDto) {
+    return ['/repo', env.repository?.id, 'ci-cd', 'pr', env.latestDeployment?.pullRequestNumber?.toString()];
+  }
+
+  getBranchLink(env: EnvironmentDto) {
+    return ['/repo', env.repository?.id, 'ci-cd', 'branch', env.latestDeployment?.ref];
+  }
 }
diff --git a/client/src/app/core/modules/openapi/schemas.gen.ts b/client/src/app/core/modules/openapi/schemas.gen.ts
index f491dce5e..6f99d1ab9 100644
--- a/client/src/app/core/modules/openapi/schemas.gen.ts
+++ b/client/src/app/core/modules/openapi/schemas.gen.ts
@@ -95,6 +95,10 @@ export const EnvironmentDeploymentSchema = {
     user: {
       $ref: '#/components/schemas/UserInfoDto',
     },
+    pullRequestNumber: {
+      type: 'integer',
+      format: 'int32',
+    },
     createdAt: {
       type: 'string',
       format: 'date-time',
diff --git a/client/src/app/core/modules/openapi/types.gen.ts b/client/src/app/core/modules/openapi/types.gen.ts
index 94234a2e6..74849adc0 100644
--- a/client/src/app/core/modules/openapi/types.gen.ts
+++ b/client/src/app/core/modules/openapi/types.gen.ts
@@ -30,6 +30,7 @@ export type EnvironmentDeployment = {
   releaseCandidateName?: string;
   prName?: string;
   user?: UserInfoDto;
+  pullRequestNumber?: number;
   createdAt?: string;
   updatedAt?: string;
   type: 'GITHUB' | 'HELIOS';
diff --git a/client/src/app/pages/ci-cd/ci-cd.component.ts b/client/src/app/pages/ci-cd/ci-cd.component.ts
index e19ac1c1e..01cd19630 100644
--- a/client/src/app/pages/ci-cd/ci-cd.component.ts
+++ b/client/src/app/pages/ci-cd/ci-cd.component.ts
@@ -1,15 +1,18 @@
-import { Component, signal, inject, OnInit, input, numberAttribute } from '@angular/core';
-import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
+import { Component, signal, inject, OnInit, input, numberAttribute, OnDestroy } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterOutlet } from '@angular/router';
 import { PageHeadingComponent } from '@app/components/page-heading/page-heading.component';
 import { TabMenuModule } from 'primeng/tabmenu';
 import { TabsModule } from 'primeng/tabs';
+import { filter, Subscription } from 'rxjs';
 @Component({
   selector: 'app-ci-cd',
   imports: [RouterLink, RouterOutlet, TabMenuModule, TabsModule, PageHeadingComponent],
   templateUrl: './ci-cd.component.html',
 })
-export class CiCdComponent implements OnInit {
+export class CiCdComponent implements OnInit, OnDestroy {
   private route = inject(ActivatedRoute);
+  private router = inject(Router);
+  private subscription = new Subscription();
 
   repositoryId = input.required({ transform: numberAttribute });
 
@@ -17,10 +20,27 @@ export class CiCdComponent implements OnInit {
     { label: 'Pull Requests', id: 'pr' },
     { label: 'Branches', id: 'branch' },
   ]);
-
-  activeTabId = this.tabs()[0].id;
+  activeTabId = signal(this.tabs()[0].id);
 
   ngOnInit() {
-    this.activeTabId = this.route.snapshot.firstChild?.url[0].path || this.tabs()[0].id;
+    this.updateActiveTab();
+
+    // Subscribe to router events to update the active tab when navigation occurs
+    this.subscription.add(
+      this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
+        this.updateActiveTab();
+      })
+    );
+  }
+
+  ngOnDestroy() {
+    this.subscription.unsubscribe();
+  }
+
+  private updateActiveTab() {
+    const path = this.route.snapshot.firstChild?.url[0]?.path;
+    if (path) {
+      this.activeTabId.set(path);
+    }
   }
 }
diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml
index e61b7abab..95804827a 100644
--- a/server/application-server/openapi.yaml
+++ b/server/application-server/openapi.yaml
@@ -1066,6 +1066,9 @@ components:
           type: string
         user:
           $ref: "#/components/schemas/UserInfoDto"
+        pullRequestNumber:
+          type: integer
+          format: int32
         createdAt:
           type: string
           format: date-time
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/LatestDeploymentUnion.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/LatestDeploymentUnion.java
index 56cf4d5ec..91d87be0d 100644
--- a/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/LatestDeploymentUnion.java
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/LatestDeploymentUnion.java
@@ -241,6 +241,20 @@ public String getPullRequestName() {
     }
   }
 
+  public Integer getPullRequestNumber() {
+    if (isRealDeployment()) {
+      return realDeployment.getPullRequest() != null
+          ? realDeployment.getPullRequest().getNumber()
+          : null;
+    } else if (isHeliosDeployment()) {
+      return heliosDeployment.getPullRequest() != null
+          ? heliosDeployment.getPullRequest().getNumber()
+          : null;
+    } else {
+      return null;
+    }
+  }
+
   public boolean isNone() {
     return !isRealDeployment() && !isHeliosDeployment();
   }
diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentDto.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentDto.java
index 81a08d272..8e902d56f 100644
--- a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentDto.java
+++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentDto.java
@@ -73,6 +73,7 @@ public static record EnvironmentDeployment(
       String releaseCandidateName,
       String prName,
       UserInfoDto user,
+      Integer pullRequestNumber,
       OffsetDateTime createdAt,
       OffsetDateTime updatedAt,
       @NonNull DeploymentType type) {
@@ -94,6 +95,7 @@ public static EnvironmentDeployment fromUnion(
               .orElse(null),
           union.getPullRequestName(),
           UserInfoDto.fromUser(union.getCreator()),
+          union.getPullRequestNumber(),
           union.getCreatedAt(),
           union.getUpdatedAt(),
           union.getType());


Reviews:
user: login: egekocabas
body: I believe the current behavior isn't ideal for user experience. From a user's perspective, clicking on the branch name should navigate directly to the branch details. One potential solution might be to introduce an additional tag showing only the PR number (e.g., `#10254`). However, in any case, we should also include a hover effect to clarify that the element is clickable and redirects the user elsewhere.

What do you think about that? I know there are already a lot of elements in this view, but I believe this approach would provide a better overall user experience.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T17:27:30Z
user: login: thielpa
body: Nice and clean! I just have a minor change, that we should address :) :partying_face: 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T21:16:02Z
user: login: gbanu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T21:46:03Z
user: login: TurkerKoc
body: LGTM! Great job, thanks for adding this feature 🚀 🎊 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T22:08:26Z

Review comments:
diff_hunk: @@ -44,13 +44,39 @@
                   <span [pTooltip]="getDeploymentTime(environment) || ''">{{ environment.latestDeployment.updatedAt || '' | timeAgo }}</span>
                 }
                 <app-deployment-state-tag [state]="deployment.state" />
-                <p-tag severity="secondary" rounded="true" class="max-w-[350px] flex items-center" [pTooltip]="tooltipTemplate" tooltipPosition="top">
-                  <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+
+                <!-- Branch or Release Tag -->
+                <p-tag
+                  severity="secondary"
path: client/src/app/components/environments/environment-list/environment-list-view.component.html
user: login: thielpa
body: issue: please add cursor-pointer class here
created_at: 2025-03-16T21:12:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,13 +44,39 @@
                   <span [pTooltip]="getDeploymentTime(environment) || ''">{{ environment.latestDeployment.updatedAt || '' | timeAgo }}</span>
                 }
                 <app-deployment-state-tag [state]="deployment.state" />
-                <p-tag severity="secondary" rounded="true" class="max-w-[350px] flex items-center" [pTooltip]="tooltipTemplate" tooltipPosition="top">
-                  <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+
+                <!-- Branch or Release Tag -->
+                <p-tag
+                  severity="secondary"
+                  rounded="true"
+                  class="max-w-[350px] flex items-center"
+                  [pTooltip]="isRelease(deployment) ? '' : 'Go to ' + deployment.ref"
+                  tooltipPosition="top"
+                  (click)="$event.stopPropagation()"
+                  [routerLink]="isRelease(deployment) ? null : getBranchLink(environment)"
+                >
+                  @if (isRelease(deployment)) {
+                    <i-tabler name="tag" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                  } @else {
+                    <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                  }
                   <span class="ml-1 truncate flex-1">{{ deployment.ref }}</span>
-                  <ng-template #tooltipTemplate>
-                    <div class="flex items-center">{{ deployment.ref }}</div>
-                  </ng-template>
                 </p-tag>
+                @if (deployment.pullRequestNumber) {
+                  <!-- PR Tag -->
+                  <p-tag
path: client/src/app/components/environments/environment-list/environment-list-view.component.html
user: login: thielpa
body: issue: please also add cursor-pointer class here
created_at: 2025-03-16T21:13:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,13 +44,39 @@
                   <span [pTooltip]="getDeploymentTime(environment) || ''">{{ environment.latestDeployment.updatedAt || '' | timeAgo }}</span>
                 }
                 <app-deployment-state-tag [state]="deployment.state" />
-                <p-tag severity="secondary" rounded="true" class="max-w-[350px] flex items-center" [pTooltip]="tooltipTemplate" tooltipPosition="top">
-                  <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+
+                <!-- Branch or Release Tag -->
+                <p-tag
+                  severity="secondary"
+                  rounded="true"
+                  class="max-w-[350px] flex items-center"
+                  [pTooltip]="isRelease(deployment) ? '' : 'Go to ' + deployment.ref"
+                  tooltipPosition="top"
+                  (click)="$event.stopPropagation()"
+                  [routerLink]="isRelease(deployment) ? null : getBranchLink(environment)"
+                >
+                  @if (isRelease(deployment)) {
+                    <i-tabler name="tag" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                  } @else {
+                    <i-tabler name="git-branch" class="!size-5 mr-0.5 flex-shrink-0"></i-tabler>
+                  }
                   <span class="ml-1 truncate flex-1">{{ deployment.ref }}</span>
-                  <ng-template #tooltipTemplate>
-                    <div class="flex items-center">{{ deployment.ref }}</div>
-                  </ng-template>
                 </p-tag>
+                @if (deployment.pullRequestNumber) {
+                  <!-- PR Tag -->
+                  <p-tag
path: client/src/app/components/environments/environment-list/environment-list-view.component.html
user: login: gbanu
body: it is actually enabled on the whole element but was not showing during screen recording 😅 
![IMG_5425](https://github.com/user-attachments/assets/fc12e9d2-a2a2-4db8-9cd6-b35d0005b346)
![IMG_5426](https://github.com/user-attachments/assets/41d4e400-93fc-426e-bd55-7d045b3bd3c2)


created_at: 2025-03-16T21:46:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: codacy-production[bot]
created_at: 2025-03-15T16:28:57Z
body: ## Coverage summary from Codacy
### [See diff coverage on Codacy](https://app.codacy.com/gh/ls1intum/Helios/pull-requests/540/coverage?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryPullRequest)

 | Coverage variation | Diff coverage | 
 | ------------- | ------------- |
 | :white_check_mark: **+0.96%** (target: -1.00%) | :white_check_mark: **0.00%** |

<details>
<summary>Coverage variation details</summary>

| | Coverable lines | Covered lines | Coverage |
| ------------- | ------------- | ------------- | ------------- |
| Common ancestor commit (f26f0a9fbdab9022dc1a207c0818f6d0937dd96f) | 4153 | 470 | 11.32% | |
| Head commit (516c4dacf55920643981067f3e2fea1db642db12) | 5138 (+985) | 631 (+161) | 12.28% (**+0.96%**) |

**Coverage variation** is the difference between the coverage for the head and common ancestor commits of the pull request branch: `<coverage of head commit> - <coverage of common ancestor commit>`
</details>

<details>
<summary>Diff coverage details</summary>

| | Coverable lines | Covered lines | Diff coverage |
| ------------- | ------------- | ------------- | ------------- |
| Pull request (#540) | 13 | 0 | **0.00%** |

**Diff coverage** is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: `<covered lines added or modified>/<coverable lines added or modified> * 100%`
</details>

#### [See your quality gate settings](https://app.codacy.com/gh/ls1intum/Helios/settings/quality?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryGates)&nbsp;&nbsp;&nbsp;&nbsp;[Change summary preferences](https://app.codacy.com/gh/ls1intum/Helios/settings/integrations?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummarySettings)
<sub>Codacy stopped sending the deprecated coverage status on June 5th, 2024. [Learn more](https://docs.codacy.com/release-notes/cloud/cloud-2023-11-23-new-coverage-engine-status-checks/#deprecation-and-removal-of-the-old-coverage-engine-status-checks)</sub>

<!-- 108f945d-e553-4918-8ccf-027c6d4acc2c -->
author_association: NONE
user: login: gbanu
created_at: 2025-03-16T16:24:12Z
body: @egekocabas added a separate tag for PR and disabled navigation for release tags, Paul has implemented the release sync from github and we can implement a link to the release once his pr is merged.


https://github.com/user-attachments/assets/8c8e176b-3f47-4dab-983b-09c16f36ea8e



author_association: CONTRIBUTOR
user: login: TurkerKoc
created_at: 2025-03-16T19:31:05Z
body: > @egekocabas added a separate tag for PR and disabled navigation for release tags, Paul has implemented the release sync from github and we can implement a link to the release once his pr is merged.
> 
>  Screen.Recording.2025-03-16.at.17.23.40.mov

That looks great thanks for adding separate link for PRs!

Maybe a minor change: I think we don't need to write PR as a prefix since this is a known pattern for developers -> #<PR_NUMBER>. Also we have the icon as well :)
author_association: MEMBER