Skip to content

Commit 17fe59e

Browse files
authored
feat: Show more details of a locked environment in tooltip (#291)
1 parent 590cfa9 commit 17fe59e

File tree

7 files changed

+120
-71
lines changed

7 files changed

+120
-71
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@if (latestLock(); as lock) {
2+
<a routerLink="environment/list" class="text-sm text-gray-500 flex flex-col items-center gap-1 mb-5" [pTooltip]="tooltipContent">
3+
<i-tabler name="lock"></i-tabler>
4+
<span class="w-20 text-center">{{ timeSinceLocked() }}</span>
5+
</a>
6+
<ng-template #tooltipContent>
7+
<div class="flex flex-col">
8+
<span class="text-xs text-gray-200 uppercase tracking-tighter font-bold">Locked Environment</span>
9+
<span class="text-sm">{{ lock.environment?.name }}</span>
10+
@if (lock.environment?.latestDeployment?.ref) {
11+
<div class="border-b border-gray-500 my-2"></div>
12+
<div class="flex gap-1 items-center">
13+
<span class="text-sm truncate">{{ lock.environment?.latestDeployment?.ref }}</span>
14+
<i-tabler style="height: 16px; width: 16px" name="git-branch" class="text-gray-200 flex-shrink-0" />
15+
</div>
16+
}
17+
</div>
18+
</ng-template>
19+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Component, inject, OnInit, OnDestroy, signal, computed } from '@angular/core';
2+
import { RouterLink } from '@angular/router';
3+
import { getEnvironmentsByUserLockingOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
4+
import { KeycloakService } from '@app/core/services/keycloak/keycloak.service';
5+
import { injectQuery } from '@tanstack/angular-query-experimental';
6+
import { IconsModule } from 'icons.module';
7+
import { TooltipModule } from 'primeng/tooltip';
8+
9+
@Component({
10+
selector: 'app-user-lock-info',
11+
imports: [IconsModule, TooltipModule, RouterLink],
12+
templateUrl: './user-lock-info.component.html',
13+
})
14+
export class UserLockInfoComponent implements OnInit, OnDestroy {
15+
private keycloakService = inject(KeycloakService);
16+
17+
timeNow = signal<Date>(new Date());
18+
private intervalId?: ReturnType<typeof setInterval>;
19+
20+
// Returns the latest lock information for the current user
21+
lockQuery = injectQuery(() => ({
22+
...getEnvironmentsByUserLockingOptions(),
23+
refetchInterval: 10000,
24+
enabled: () => !!this.keycloakService.isLoggedIn(),
25+
}));
26+
27+
ngOnInit() {
28+
this.intervalId = setInterval(() => {
29+
this.timeNow.set(new Date());
30+
}, 1000);
31+
}
32+
33+
ngOnDestroy() {
34+
// Clear the interval
35+
if (this.intervalId) {
36+
clearInterval(this.intervalId);
37+
}
38+
}
39+
40+
latestLock = computed(() => {
41+
const lock = this.lockQuery.data();
42+
43+
// For some reason, when there is no lock
44+
// an empty object instead of null is returned
45+
return lock?.lockedAt ? lock : null;
46+
});
47+
48+
timeSinceLocked = computed(() => {
49+
const latestLock = this.latestLock();
50+
51+
if (!latestLock?.lockedAt) return '';
52+
53+
const lockedDate = new Date(latestLock.lockedAt);
54+
const now = this.timeNow(); // your signal for "current time"
55+
const diffMs = now.getTime() - lockedDate.getTime();
56+
57+
// If lockedAt is in the future, return empty or handle differently
58+
if (diffMs < 0) {
59+
return '';
60+
}
61+
62+
const totalSeconds = Math.floor(diffMs / 1000);
63+
const days = Math.floor(totalSeconds / 86400); // 1 day = 86400s
64+
const hours = Math.floor((totalSeconds % 86400) / 3600);
65+
const minutes = Math.floor((totalSeconds % 3600) / 60);
66+
const seconds = totalSeconds % 60;
67+
68+
// Zero-pad hours, minutes, seconds
69+
const padTwo = (num: number) => num.toString().padStart(2, '0');
70+
71+
if (days > 0) {
72+
// Format: dd:hh:mm (e.g., "1:05:12" => 1 day, 5 hours, 12 minutes)
73+
const dd = days.toString(); // or padTwo(days) if you prefer "01" for 1 day
74+
const hh = padTwo(hours);
75+
const mm = padTwo(minutes);
76+
return `${dd}:${hh}:${mm}`;
77+
} else {
78+
// Format: hh:mm:ss (e.g., "05:12:36")
79+
const hh = padTwo(hours);
80+
const mm = padTwo(minutes);
81+
const ss = padTwo(seconds);
82+
return `${hh}:${mm}:${ss}`;
83+
}
84+
});
85+
}

client/src/app/pages/main-layout/main-layout.component.html

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@
1919
}
2020
</div>
2121
<span class="flex-grow"></span>
22-
@if (lockQuery.data()?.lockedAt) {
23-
<p class="text-sm text-gray-500 flex items-center gap-1 mb-5">
24-
<i-tabler name="lock"></i-tabler>
25-
<span class="w-20">{{ timeSinceLocked(lockQuery.data()?.lockedAt) }}</span>
26-
</p>
27-
}
22+
<app-user-lock-info />
2823
<app-profile-nav-section />
2924
</div>
3025

Lines changed: 5 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { SlicePipe } from '@angular/common';
2-
import { Component, OnDestroy, OnInit, computed, inject, input, numberAttribute, signal } from '@angular/core';
2+
import { Component, computed, inject, input, numberAttribute } from '@angular/core';
33
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
44
import { ProfileNavSectionComponent } from '@app/components/profile-nav-section/profile-nav-section.component';
5-
import { getEnvironmentsByUserLockingOptions, getRepositoryByIdOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
5+
import { getRepositoryByIdOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
66
import { KeycloakService } from '@app/core/services/keycloak/keycloak.service';
77
import { PermissionService } from '@app/core/services/permission.service';
88
import { injectQuery } from '@tanstack/angular-query-experimental';
@@ -14,6 +14,7 @@ import { DividerModule } from 'primeng/divider';
1414
import { ToastModule } from 'primeng/toast';
1515
import { TooltipModule } from 'primeng/tooltip';
1616
import { HeliosIconComponent } from '../../components/helios-icon/helios-icon.component';
17+
import { UserLockInfoComponent } from '@app/components/user-lock-info/user-lock-info.component';
1718

1819
@Component({
1920
selector: 'app-main-layout',
@@ -32,10 +33,11 @@ import { HeliosIconComponent } from '../../components/helios-icon/helios-icon.co
3233
AvatarModule,
3334
CardModule,
3435
ProfileNavSectionComponent,
36+
UserLockInfoComponent,
3537
],
3638
templateUrl: './main-layout.component.html',
3739
})
38-
export class MainLayoutComponent implements OnInit, OnDestroy {
40+
export class MainLayoutComponent {
3941
private keycloakService = inject(KeycloakService);
4042
private permissionService = inject(PermissionService);
4143

@@ -48,15 +50,6 @@ export class MainLayoutComponent implements OnInit, OnDestroy {
4850
enabled: () => !!this.repositoryId(),
4951
}));
5052

51-
lockQuery = injectQuery(() => ({
52-
...getEnvironmentsByUserLockingOptions(),
53-
refetchInterval: 10000,
54-
enabled: () => !!this.keycloakService.isLoggedIn(),
55-
}));
56-
57-
timeNow = signal<Date>(new Date());
58-
private intervalId?: ReturnType<typeof setInterval>;
59-
6053
logout() {
6154
this.keycloakService.logout();
6255
}
@@ -94,53 +87,4 @@ export class MainLayoutComponent implements OnInit, OnDestroy {
9487
];
9588
return baseItems;
9689
});
97-
98-
ngOnInit() {
99-
this.intervalId = setInterval(() => {
100-
this.timeNow.set(new Date());
101-
}, 1000);
102-
}
103-
104-
ngOnDestroy() {
105-
// Clear the interval
106-
if (this.intervalId) {
107-
clearInterval(this.intervalId);
108-
}
109-
}
110-
111-
timeSinceLocked(lockedAt: string | undefined): string {
112-
if (!lockedAt) return '';
113-
114-
const lockedDate = new Date(lockedAt);
115-
const now = this.timeNow(); // your signal for "current time"
116-
const diffMs = now.getTime() - lockedDate.getTime();
117-
118-
// If lockedAt is in the future, return empty or handle differently
119-
if (diffMs < 0) {
120-
return '';
121-
}
122-
123-
const totalSeconds = Math.floor(diffMs / 1000);
124-
const days = Math.floor(totalSeconds / 86400); // 1 day = 86400s
125-
const hours = Math.floor((totalSeconds % 86400) / 3600);
126-
const minutes = Math.floor((totalSeconds % 3600) / 60);
127-
const seconds = totalSeconds % 60;
128-
129-
// Zero-pad hours, minutes, seconds
130-
const padTwo = (num: number) => num.toString().padStart(2, '0');
131-
132-
if (days > 0) {
133-
// Format: dd:hh:mm (e.g., "1:05:12" => 1 day, 5 hours, 12 minutes)
134-
const dd = days.toString(); // or padTwo(days) if you prefer "01" for 1 day
135-
const hh = padTwo(hours);
136-
const mm = padTwo(minutes);
137-
return `${dd}:${hh}:${mm}`;
138-
} else {
139-
// Format: hh:mm:ss (e.g., "05:12:36")
140-
const hh = padTwo(hours);
141-
const mm = padTwo(minutes);
142-
const ss = padTwo(seconds);
143-
return `${hh}:${mm}:${ss}`;
144-
}
145-
}
14690
}

server/application-server/src/main/java/de/tum/cit/aet/helios/environment/Environment.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import jakarta.persistence.Version;
1616
import java.time.OffsetDateTime;
1717
import java.util.List;
18+
import java.util.Optional;
1819
import lombok.Getter;
1920
import lombok.NoArgsConstructor;
2021
import lombok.Setter;
@@ -84,6 +85,10 @@ public class Environment extends RepositoryFilterEntity {
8485
@OneToMany(mappedBy = "environment", fetch = FetchType.LAZY)
8586
private List<EnvironmentLockHistory> lockHistory;
8687

88+
public Optional<Deployment> getLatestDeployment() {
89+
return this.deployments.reversed().stream().findFirst();
90+
}
91+
8792
// Missing properties
8893
// nodeId --> GraphQl ID
8994
// ProtectionRule

server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentLockHistoryDto.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ public record EnvironmentLockHistoryDto(
1313
@Nullable OffsetDateTime unlockedAt,
1414
EnvironmentDto environment) {
1515

16-
1716
public static EnvironmentLockHistoryDto fromEnvironmentLockHistory(
1817
EnvironmentLockHistory environmentLockHistory) {
1918
Environment environment = environmentLockHistory.getEnvironment();
@@ -22,6 +21,8 @@ public static EnvironmentLockHistoryDto fromEnvironmentLockHistory(
2221
environmentLockHistory.getLockedBy(),
2322
environmentLockHistory.getLockedAt(),
2423
environmentLockHistory.getUnlockedAt(),
25-
EnvironmentDto.fromEnvironment(environment));
24+
EnvironmentDto.fromEnvironment(
25+
environment,
26+
environment.getLatestDeployment()));
2627
}
2728
}

server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public List<EnvironmentDto> getAllEnvironments() {
4242
.map(
4343
environment -> {
4444
return EnvironmentDto.fromEnvironment(
45-
environment, environment.getDeployments().reversed().stream().findFirst());
45+
environment, environment.getLatestDeployment());
4646
})
4747
.collect(Collectors.toList());
4848
}
@@ -52,7 +52,7 @@ public List<EnvironmentDto> getAllEnabledEnvironments() {
5252
.map(
5353
environment -> {
5454
return EnvironmentDto.fromEnvironment(
55-
environment, environment.getDeployments().reversed().stream().findFirst());
55+
environment, environment.getLatestDeployment());
5656
})
5757
.collect(Collectors.toList());
5858
}

0 commit comments

Comments
 (0)