From 4635a3bd09840123457efe56234c8ec06b1ba3d4 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:09:33 +0100 Subject: [PATCH 01/14] deployment stepper created --- .../deployment-stepper.component.html | 62 +++++++++++++++ .../deployment-stepper.component.ts | 77 +++++++++++++++++++ .../environment-list-view.component.html | 3 + .../environment-list-view.component.ts | 3 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html create mode 100644 client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html new file mode 100644 index 00000000..5731a567 --- /dev/null +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html @@ -0,0 +1,62 @@ +
+
+ @for (step of steps; track step; let i = $index) { +
+
+ +
+ @switch (getStepStatus(i)) { + @case ('completed') { + + } + @case ('active') { + + } + @case ('error') { + + } + @case ('upcoming') { +
+ } + } +
+ + +
+ {{ step }} +
+ {{ stepDescriptions[step] }} +
+ +
+ @if (getTimeEstimate(i)) { + {{ getTimeEstimate(i) }} remaining + } +
+
+
+
+ } +
+ + +
+
+
+ + @if (isErrorState) { +
+ + Deployment failed - {{ deployment?.state }} +
+ } +
diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts new file mode 100644 index 00000000..9d7ac84d --- /dev/null +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts @@ -0,0 +1,77 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IconsModule } from 'icons.module'; +import { EnvironmentDeployment } from '@app/core/modules/openapi'; + +@Component({ + selector: 'app-deployment-stepper', + imports: [CommonModule, IconsModule], + templateUrl: './deployment-stepper.component.html', +}) +export class DeploymentStepperComponent { + @Input() deployment: EnvironmentDeployment | undefined; + + steps: ('WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS')[] = ['WAITING', 'PENDING', 'IN_PROGRESS', 'SUCCESS']; + stepDescriptions: { [key in 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS']: string } = { + WAITING: 'Waiting for approval', + PENDING: 'Pending deployment', + IN_PROGRESS: 'Deployment in progress', + SUCCESS: 'Deployment successful', + }; + + estimatedTimes = { + WAITING: 1, // minutes + PENDING: 2, + IN_PROGRESS: 5, + }; + + get currentStepIndex(): number { + if (!this.deployment?.state) return -1; + const state = this.deployment.state as 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS'; + return this.steps.indexOf(state); + } + + get isErrorState(): boolean { + return ['ERROR', 'FAILURE'].includes(this.deployment?.state || ''); + } + + getStepStatus(index: number): string { + if (index === 3 && this.currentStepIndex === 3) return 'completed'; + if (this.currentStepIndex < 0) return 'upcoming'; + if (index < this.currentStepIndex) return 'completed'; + if (index === this.currentStepIndex) return this.isErrorState ? 'error' : 'active'; + return 'upcoming'; + } + + get progressPercentage(): number { + if (this.currentStepIndex < 0) return 0; + // Calculate percentage based on completed steps + return (this.currentStepIndex / (this.steps.length - 1)) * 100; + } + + getTimeEstimate(index: number): string { + // Show estimates for current and upcoming steps only + if (index < this.currentStepIndex) return ''; + + const step = this.steps[index]; + const estimate = this.estimatedTimes[step as keyof typeof this.estimatedTimes]; + + // For completed steps, show actual duration if available + if (index === this.currentStepIndex && this.deployment?.updatedAt) { + const duration = this.calculateStepDuration(); + if (duration) return `${duration}m`; + } + + return estimate ? `${estimate}m` : ''; + } + + private calculateStepDuration(): number | null { + if (!this.deployment?.createdAt) return null; + + const created = new Date(this.deployment.createdAt).getTime(); + const now = Date.now(); + const minutes = Math.round((now - created) / 60000); + + return minutes; + } +} 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 99348b1b..af6f80a2 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 @@ -109,6 +109,9 @@ } + @if (environment.latestDeployment; as deployment) { + + }
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 984cada3..b88e5168 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 @@ -31,7 +31,7 @@ import { TimeAgoPipe } from '@app/pipes/time-ago.pipe'; import { UserAvatarComponent } from '@app/components/user-avatar/user-avatar.component'; import { EnvironmentStatusInfoComponent } from '../environment-status-info/environment-status-info.component'; import { EnvironmentStatusTagComponent } from '../environment-status-tag/environment-status-tag.component'; - +import { DeploymentStepperComponent } from '../deployment-stepper/deployment-stepper.component'; @Component({ selector: 'app-environment-list-view', imports: [ @@ -44,6 +44,7 @@ import { EnvironmentStatusTagComponent } from '../environment-status-tag/environ ButtonModule, TooltipModule, DeploymentStateTagComponent, + DeploymentStepperComponent, EnvironmentStatusTagComponent, EnvironmentDeploymentInfoComponent, EnvironmentStatusInfoComponent, From 59b81251ea99b0e568ae3d3782635d0b5b91c81e Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Sat, 15 Feb 2025 16:02:46 +0100 Subject: [PATCH 02/14] deployment stepper with dynamic times and progress bar --- .../deployment-stepper.component.html | 50 ++--- .../deployment-stepper.component.ts | 180 ++++++++++++++---- 2 files changed, 168 insertions(+), 62 deletions(-) diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html index 5731a567..d88b4701 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html @@ -1,5 +1,9 @@
-
+ + + + +
@for (step of steps; track step; let i = $index) {
@@ -11,8 +15,8 @@ 'bg-red-500': getStepStatus(i) === 'error', 'bg-gray-200': getStepStatus(i) === 'upcoming', }" - class="w-8 h-8 rounded-full flex items-center justify-center transition-colors duration-300 mt-3" - [style.z-index]="1" + class="w-8 h-8 rounded-full flex items-center justify-center transition-colors duration-300" + style="z-index: 1" > @switch (getStepStatus(i)) { @case ('completed') { @@ -30,33 +34,35 @@ }
- -
- {{ step }} -
- {{ stepDescriptions[step] }} -
- -
- @if (getTimeEstimate(i)) { - {{ getTimeEstimate(i) }} remaining - } -
+ +
+ @if (getStepStatus(i) === 'error') { + {{ 'ERROR' }} +
+ {{ stepDescriptions['ERROR'] }} +
+ } @else { + {{ step }} +
+ {{ stepDescriptions[step] }} +
+
+ @if (getTimeEstimate(i)) { + {{ getTimeEstimate(i) }} remaining + } +
+ }
}
- -
-
-
- +
diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts index 9d7ac84d..5197ea46 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts @@ -1,77 +1,177 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IconsModule } from 'icons.module'; +import { ProgressBarModule } from 'primeng/progressbar'; import { EnvironmentDeployment } from '@app/core/modules/openapi'; @Component({ selector: 'app-deployment-stepper', - imports: [CommonModule, IconsModule], + imports: [CommonModule, IconsModule, ProgressBarModule], templateUrl: './deployment-stepper.component.html', }) -export class DeploymentStepperComponent { +export class DeploymentStepperComponent implements OnInit, OnDestroy { @Input() deployment: EnvironmentDeployment | undefined; + // Define the four steps. (Note: estimatedTimes are defined only for the first three.) steps: ('WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS')[] = ['WAITING', 'PENDING', 'IN_PROGRESS', 'SUCCESS']; - stepDescriptions: { [key in 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS']: string } = { + stepDescriptions: { + [key in 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS' | 'ERROR' | 'FAILURE']: string; + } = { WAITING: 'Waiting for approval', PENDING: 'Pending deployment', IN_PROGRESS: 'Deployment in progress', SUCCESS: 'Deployment successful', + ERROR: 'Deployment error', + FAILURE: 'Deployment failed', }; + // Estimated times in minutes for each non-terminal step. estimatedTimes = { - WAITING: 1, // minutes - PENDING: 2, + WAITING: 1, + PENDING: 1, IN_PROGRESS: 5, }; - get currentStepIndex(): number { - if (!this.deployment?.state) return -1; - const state = this.deployment.state as 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS'; - return this.steps.indexOf(state); + // A timer updated every second to drive the dynamic progress. + time: number = Date.now(); + intervalId: number | undefined; + + ngOnInit(): void { + this.intervalId = window.setInterval(() => { + this.time = Date.now(); + }, 1000); } - get isErrorState(): boolean { - return ['ERROR', 'FAILURE'].includes(this.deployment?.state || ''); + ngOnDestroy(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + } } - getStepStatus(index: number): string { - if (index === 3 && this.currentStepIndex === 3) return 'completed'; - if (this.currentStepIndex < 0) return 'upcoming'; - if (index < this.currentStepIndex) return 'completed'; - if (index === this.currentStepIndex) return this.isErrorState ? 'error' : 'active'; - return 'upcoming'; + /** + * Calculates a virtual step index based solely on elapsed time. + * - If elapsed time is less than WAITING's estimated time, returns 0. + * - If elapsed time is between WAITING and WAITING+PENDING, returns 1. + * - If between WAITING+PENDING and WAITING+PENDING+IN_PROGRESS, returns 2. + * - Otherwise, returns 3 (i.e. SUCCESS). + */ + get virtualStepIndex(): number { + if (!this.deployment || !this.deployment.createdAt) return 0; + const start = new Date(this.deployment.createdAt).getTime(); + const elapsedMinutes = (this.time - start) / 60000; + if (elapsedMinutes >= this.estimatedTimes.WAITING + this.estimatedTimes.PENDING + this.estimatedTimes.IN_PROGRESS) { + return 3; + } else if (elapsedMinutes >= this.estimatedTimes.WAITING + this.estimatedTimes.PENDING) { + return 2; + } else if (elapsedMinutes >= this.estimatedTimes.WAITING) { + return 1; + } else { + return 0; + } } - get progressPercentage(): number { - if (this.currentStepIndex < 0) return 0; - // Calculate percentage based on completed steps - return (this.currentStepIndex / (this.steps.length - 1)) * 100; + /** + * Determines the effective step index. + * If the deployment state is terminal (SUCCESS, ERROR, FAILURE), use that state. + * Otherwise, use the virtual (elapsed-time based) step. + */ + get currentEffectiveStepIndex(): number { + if (!this.deployment || !this.deployment.createdAt) return 0; + // if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { + const index = this.steps.indexOf(this.deployment.state as 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS'); + return index !== -1 ? index : 3; + // } + // return this.virtualStepIndex; } - getTimeEstimate(index: number): string { - // Show estimates for current and upcoming steps only - if (index < this.currentStepIndex) return ''; + get isErrorState(): boolean { + return ['ERROR', 'FAILURE'].includes(this.deployment?.state || ''); + } - const step = this.steps[index]; - const estimate = this.estimatedTimes[step as keyof typeof this.estimatedTimes]; + /** + * Returns a status string for a given step index. + * - "completed" if the step index is less than the current effective step. + * - "active" if it matches the current effective step (or "error" if in error state). + * - "upcoming" otherwise. + */ + getStepStatus(index: number): string { + const effectiveStep = this.currentEffectiveStepIndex; + if (index < effectiveStep) return 'completed'; + if (index === effectiveStep) return this.isErrorState ? 'error' : 'active'; + return 'upcoming'; + } - // For completed steps, show actual duration if available - if (index === this.currentStepIndex && this.deployment?.updatedAt) { - const duration = this.calculateStepDuration(); - if (duration) return `${duration}m`; + /** + * Computes overall progress in a piecewise manner. + * Each segment (WAITING, PENDING, IN_PROGRESS) is allotted 25% of the bar. + * For the current segment, progress is calculated as a ratio of its elapsed time; + * if a segment is already completed (i.e. virtualStepIndex > segment index), that segment is full. + * When the virtual step reaches 3, the progress returns 100%. + */ + get dynamicProgress(): number { + if (!this.deployment || !this.deployment.createdAt) return 0; + // If the server indicates a terminal state, show full progress. + if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { + return 100; } + const start = new Date(this.deployment.createdAt).getTime(); + const elapsedMs = this.time - start; + // If the virtual step is 3, show 100%. + if (this.currentEffectiveStepIndex === 3) return 100; - return estimate ? `${estimate}m` : ''; + let progress = 0; + let cumulativeEstimateMs = 0; + // There are three segments: 0 (0–25%), 1 (25–50%), 2 (50–75%). + for (let i = 0; i < 3; i++) { + const segStart = i * 25; + const segEnd = (i + 1) * 25; + const estimatedMs = this.estimatedTimes[this.steps[i] as keyof typeof this.estimatedTimes] * 60000; + // If this step is fully complete, assign the full segment. + if (this.currentEffectiveStepIndex > i) { + progress = segEnd; + cumulativeEstimateMs += estimatedMs; + } + // If we're in the middle of this segment, compute the ratio. + else if (this.currentEffectiveStepIndex === i) { + const elapsedForSegmentMs = elapsedMs - cumulativeEstimateMs; + let ratio = elapsedForSegmentMs / estimatedMs; + console.log('elapsed for segment ', elapsedForSegmentMs); + console.log('estimated ms ', estimatedMs); + if (ratio > 1) { + ratio = 1; + } + progress = segStart + ratio * 25; + console.log('progress ', progress); + break; + } else { + break; + } + } + return Math.floor(progress); } - private calculateStepDuration(): number | null { - if (!this.deployment?.createdAt) return null; - - const created = new Date(this.deployment.createdAt).getTime(); - const now = Date.now(); - const minutes = Math.round((now - created) / 60000); - - return minutes; + /** + * Returns the estimated time remaining (in minutes) for a given step. + * It subtracts the elapsed time from the cumulative estimated time up to that step. + */ + getTimeEstimate(index: number): string { + if (!this.deployment || !this.deployment.createdAt) return ''; + const start = new Date(this.deployment.createdAt).getTime(); + const elapsedMinutes = (this.time - start) / 60000; + let cumulative = 0; + for (let i = 0; i < 3; i++) { + const est = this.estimatedTimes[this.steps[i] as keyof typeof this.estimatedTimes]; + cumulative += est; + if (i === index) { + if (this.getStepStatus(i) === 'completed') { + return '0m 0s'; + } + const remaining = cumulative - elapsedMinutes; + const minutes = Math.floor(remaining); + const seconds = Math.floor((remaining - minutes) * 60); + return remaining > 0 ? `${minutes}m ${seconds}s` : '0m 0s'; + } + } + return ''; } } From 2eac40af777d506825dbf8631a9feeb9a671b72e Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:23:03 +0100 Subject: [PATCH 03/14] deployment stepper adjustments --- .../deployment-stepper.component.html | 17 +++- .../deployment-stepper.component.ts | 86 +++++++------------ 2 files changed, 46 insertions(+), 57 deletions(-) diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html index d88b4701..427fc375 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html @@ -1,11 +1,11 @@ -
+
@for (step of steps; track step; let i = $index) { -
+
} + @case ('unknown') { + + } + @case ('inactive') { + + } @case ('upcoming') {
} @@ -41,6 +47,11 @@
{{ stepDescriptions['ERROR'] }}
+ } @else if (isUnknownState()) { + {{ 'UNKNOWN' }} +
+ {{ stepDescriptions['UNKNOWN'] }} +
} @else { {{ step }}
diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts index 5197ea46..0c1476d1 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts @@ -15,18 +15,22 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { // Define the four steps. (Note: estimatedTimes are defined only for the first three.) steps: ('WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS')[] = ['WAITING', 'PENDING', 'IN_PROGRESS', 'SUCCESS']; stepDescriptions: { - [key in 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS' | 'ERROR' | 'FAILURE']: string; + [key in 'QUEUED' | 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS' | 'ERROR' | 'FAILURE' | 'UNKNOWN' | 'INACTIVE']: string; } = { + QUEUED: 'Deployment Queued', WAITING: 'Waiting for approval', PENDING: 'Pending deployment', IN_PROGRESS: 'Deployment in progress', SUCCESS: 'Deployment successful', ERROR: 'Deployment error', FAILURE: 'Deployment failed', + UNKNOWN: 'State unknown', + INACTIVE: 'Inactive deployment', }; // Estimated times in minutes for each non-terminal step. estimatedTimes = { + QUEUED: 5, WAITING: 1, PENDING: 1, IN_PROGRESS: 5, @@ -48,28 +52,6 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { } } - /** - * Calculates a virtual step index based solely on elapsed time. - * - If elapsed time is less than WAITING's estimated time, returns 0. - * - If elapsed time is between WAITING and WAITING+PENDING, returns 1. - * - If between WAITING+PENDING and WAITING+PENDING+IN_PROGRESS, returns 2. - * - Otherwise, returns 3 (i.e. SUCCESS). - */ - get virtualStepIndex(): number { - if (!this.deployment || !this.deployment.createdAt) return 0; - const start = new Date(this.deployment.createdAt).getTime(); - const elapsedMinutes = (this.time - start) / 60000; - if (elapsedMinutes >= this.estimatedTimes.WAITING + this.estimatedTimes.PENDING + this.estimatedTimes.IN_PROGRESS) { - return 3; - } else if (elapsedMinutes >= this.estimatedTimes.WAITING + this.estimatedTimes.PENDING) { - return 2; - } else if (elapsedMinutes >= this.estimatedTimes.WAITING) { - return 1; - } else { - return 0; - } - } - /** * Determines the effective step index. * If the deployment state is terminal (SUCCESS, ERROR, FAILURE), use that state. @@ -84,10 +66,14 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { // return this.virtualStepIndex; } - get isErrorState(): boolean { + isErrorState(): boolean { return ['ERROR', 'FAILURE'].includes(this.deployment?.state || ''); } + isUnknownState(): boolean { + return ['UNKNOWN', 'INACTIVE'].includes(this.deployment?.state || ''); + } + /** * Returns a status string for a given step index. * - "completed" if the step index is less than the current effective step. @@ -96,8 +82,9 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { */ getStepStatus(index: number): string { const effectiveStep = this.currentEffectiveStepIndex; - if (index < effectiveStep) return 'completed'; - if (index === effectiveStep) return this.isErrorState ? 'error' : 'active'; + if (this.isUnknownState()) return 'unknown'; // all steps should be unkown + if (index < effectiveStep) return 'completed'; //it's already done + if (index === effectiveStep) return this.isErrorState() ? 'error' : this.steps[index] == 'SUCCESS' ? 'completed' : 'active'; return 'upcoming'; } @@ -111,42 +98,31 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { get dynamicProgress(): number { if (!this.deployment || !this.deployment.createdAt) return 0; // If the server indicates a terminal state, show full progress. - if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { + if (['UNKNOWN', 'INACTIVE'].includes(this.deployment.state || '')) { + return 0; + } else if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { return 100; } - const start = new Date(this.deployment.createdAt).getTime(); - const elapsedMs = this.time - start; + const start = new Date(this.deployment.createdAt).getTime(); // start time is when the deployment was created + const elapsedMs = this.time - start > 0 ? this.time - start : 0; // elapsed time // If the virtual step is 3, show 100%. if (this.currentEffectiveStepIndex === 3) return 100; let progress = 0; let cumulativeEstimateMs = 0; // There are three segments: 0 (0–25%), 1 (25–50%), 2 (50–75%). - for (let i = 0; i < 3; i++) { - const segStart = i * 25; - const segEnd = (i + 1) * 25; - const estimatedMs = this.estimatedTimes[this.steps[i] as keyof typeof this.estimatedTimes] * 60000; - // If this step is fully complete, assign the full segment. - if (this.currentEffectiveStepIndex > i) { - progress = segEnd; - cumulativeEstimateMs += estimatedMs; - } - // If we're in the middle of this segment, compute the ratio. - else if (this.currentEffectiveStepIndex === i) { - const elapsedForSegmentMs = elapsedMs - cumulativeEstimateMs; - let ratio = elapsedForSegmentMs / estimatedMs; - console.log('elapsed for segment ', elapsedForSegmentMs); - console.log('estimated ms ', estimatedMs); - if (ratio > 1) { - ratio = 1; - } - progress = segStart + ratio * 25; - console.log('progress ', progress); - break; - } else { - break; - } + const segStart = this.currentEffectiveStepIndex * 33; + const estimatedMs = this.estimatedTimes[this.steps[this.currentEffectiveStepIndex] as keyof typeof this.estimatedTimes] * 60000; + + const elapsedForSegmentMs = elapsedMs - cumulativeEstimateMs > 0 ? elapsedMs - cumulativeEstimateMs : 0; + let ratio = elapsedForSegmentMs / estimatedMs; + console.log('elapsed for segment ', elapsedForSegmentMs); + console.log('estimated ms ', estimatedMs); + if (ratio > 1) { + ratio = 1; } + progress = segStart + ratio * 33; + console.log('progress ', progress); return Math.floor(progress); } @@ -161,7 +137,9 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { let cumulative = 0; for (let i = 0; i < 3; i++) { const est = this.estimatedTimes[this.steps[i] as keyof typeof this.estimatedTimes]; - cumulative += est; + if (this.getStepStatus(i) !== 'completed') { + cumulative += est; + } if (i === index) { if (this.getStepStatus(i) === 'completed') { return '0m 0s'; From f7958a0a87ea7b5a894b51f1f41a92c3ab4c8c47 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:33:41 +0100 Subject: [PATCH 04/14] always send helios deployment createdAt --- .../cit/aet/helios/deployment/LatestDeploymentUnion.java | 6 ++++++ .../tum/cit/aet/helios/environment/EnvironmentService.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) 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 f87a9924..3f46bd88 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 @@ -15,6 +15,12 @@ private LatestDeploymentUnion(Deployment realDeployment, HeliosDeployment helios this.heliosDeployment = heliosDeployment; } + public static LatestDeploymentUnion realDeployment( + Deployment dep, OffsetDateTime heliosDeploymentCreatedAt) { + dep.setCreatedAt(heliosDeploymentCreatedAt); + return new LatestDeploymentUnion(dep, null); + } + public static LatestDeploymentUnion realDeployment(Deployment dep) { return new LatestDeploymentUnion(dep, null); } diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java index 4ee11f22..16361b57 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java @@ -126,7 +126,7 @@ public LatestDeploymentUnion findLatestDeployment(Environment env) { // Compare updatedAt timestamps to determine the latest if (latestDeployment.getCreatedAt().isAfter(latestHelios.getCreatedAt())) { - return LatestDeploymentUnion.realDeployment(latestDeployment); + return LatestDeploymentUnion.realDeployment(latestDeployment, latestHelios.getCreatedAt()); } else { return LatestDeploymentUnion.heliosDeployment(latestHelios); } From 0840d062950cdbf757dff948171043295cbaf1b9 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:20:39 +0100 Subject: [PATCH 05/14] stepper harmonize --- .../deployment-stepper.component.html | 168 +++++++++++------- .../deployment-stepper.component.ts | 47 ++++- .../environment-list-view.component.html | 73 +++++--- .../environment-list-view.component.ts | 18 ++ .../environment/EnvironmentService.java | 2 +- .../heliosdeployment/HeliosDeployment.java | 9 +- .../github/GitHubWorkflowRunSyncService.java | 36 ++-- 7 files changed, 236 insertions(+), 117 deletions(-) diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html index 427fc375..2f23f7ed 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html @@ -1,79 +1,117 @@ -
- +
+ Latest Deployment Progress + + +
+ @if (deployment && deployment.state === 'SUCCESS') { +
+
+ + + {{ getDeploymentDuration() }} + +
+
(Deployment completed in)
+
+ } @else if (deployment && isErrorState()) { +
+
+ + + {{ getDeploymentDuration() }} + +
+
(Deployment failed in)
+
+ } @else if (deployment && isUnknownState()) { +
+
+ + 0m 0s +
+
(Unknown deployment duration)
+
+ } @else { +
+
+ + + {{ getTimeEstimate(2) }} + +
+
(Estimated time remaining)
+
+ } +
+ +
@for (step of steps; track step; let i = $index) {
-
- -
- @switch (getStepStatus(i)) { - @case ('completed') { - - } - @case ('active') { - - } - @case ('error') { - - } - @case ('unknown') { - - } - @case ('inactive') { - - } - @case ('upcoming') { -
- } + +
+ @switch (getStepStatus(i)) { + @case ('completed') { + } -
- - -
- @if (getStepStatus(i) === 'error') { - {{ 'ERROR' }} -
- {{ stepDescriptions['ERROR'] }} -
- } @else if (isUnknownState()) { - {{ 'UNKNOWN' }} -
- {{ stepDescriptions['UNKNOWN'] }} -
- } @else { - {{ step }} -
- {{ stepDescriptions[step] }} -
-
- @if (getTimeEstimate(i)) { - {{ getTimeEstimate(i) }} remaining - } -
+ @case ('active') { + + } + @case ('error') { + } -
+ @case ('unknown') { + + } + @case ('inactive') { + + } + @case ('upcoming') { +
+ } + } +
+ + +
+ @if (getStepStatus(i) === 'error') { + {{ 'ERROR' }} +
+ {{ stepDescriptions['ERROR'] }} +
+ } @else if (isUnknownState()) { + {{ 'UNKNOWN' }} +
+ {{ stepDescriptions['UNKNOWN'] }} +
+ } @else { + + {{ getStepDisplayName(step) }} + +
+ {{ stepDescriptions[step] }} +
+
+ @if (getTimeEstimate(i)) { + {{ getTimeEstimate(i) }} remaining + } +
+ }
}
- +
* All time estimates are approximate and based on historical data
diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts index 0c1476d1..f09d06fb 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts @@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common'; import { IconsModule } from 'icons.module'; import { ProgressBarModule } from 'primeng/progressbar'; import { EnvironmentDeployment } from '@app/core/modules/openapi'; +import { TooltipModule } from 'primeng/tooltip'; @Component({ selector: 'app-deployment-stepper', - imports: [CommonModule, IconsModule, ProgressBarModule], + imports: [CommonModule, IconsModule, ProgressBarModule, TooltipModule], templateUrl: './deployment-stepper.component.html', }) export class DeploymentStepperComponent implements OnInit, OnDestroy { @@ -18,8 +19,8 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { [key in 'QUEUED' | 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS' | 'ERROR' | 'FAILURE' | 'UNKNOWN' | 'INACTIVE']: string; } = { QUEUED: 'Deployment Queued', - WAITING: 'Waiting for approval', - PENDING: 'Pending deployment', + WAITING: 'Request sent to Github', + PENDING: 'Preparing deployment', IN_PROGRESS: 'Deployment in progress', SUCCESS: 'Deployment successful', ERROR: 'Deployment error', @@ -142,14 +143,50 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { } if (i === index) { if (this.getStepStatus(i) === 'completed') { - return '0m 0s'; + return ''; } const remaining = cumulative - elapsedMinutes; const minutes = Math.floor(remaining); const seconds = Math.floor((remaining - minutes) * 60); - return remaining > 0 ? `${minutes}m ${seconds}s` : '0m 0s'; + return remaining > 0 ? `${minutes}m ${seconds}s` : ''; } } return ''; } + + getStepDisplayName(step: string): string { + return ( + { + WAITING: 'REQUESTED', + PENDING: 'PRE-DEPLOYMENT', + IN_PROGRESS: 'DEPLOYING', + SUCCESS: 'SUCCESS', + }[step] || step + ); + } + + /** + * Computes the deployment duration by subtracting the deployment creation time + * from the finish time (or current time if finishedAt isn’t available). + */ + getDeploymentDuration(): string { + if (!this.deployment || !this.deployment.createdAt) return ''; + + // For terminal states, use finishedAt if available; otherwise use current time. + let endTime: number; + if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { + endTime = this.deployment.updatedAt ? new Date(this.deployment.updatedAt).getTime() : this.time; + } else { + // For ongoing deployments, we use the current time. + endTime = this.time; + } + + const startTime = new Date(this.deployment.createdAt).getTime(); + const elapsedMs = endTime - startTime; + if (elapsedMs < 0) return '0m 0s'; + + const minutes = Math.floor(elapsedMs / 60000); + const seconds = Math.floor((elapsedMs % 60000) / 1000); + return `${minutes}m ${seconds}s`; + } } 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 af6f80a2..71694e0c 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 @@ -96,38 +96,53 @@
-
-
- @if (environment.latestDeployment; as deployment) { - - } +
+ +
+ Details + + Deployment
-
-
- @if (environment.latestStatus; as status) { - - } -
- - @if (environment.latestDeployment; as deployment) { - + + + @if (showLatestDeployment) { +
+ +
+ } @else { +
+
+ @if (environment.latestDeployment; as deployment) { + + } +
+
+
+ @if (environment.latestStatus; as status) { + + } +
+
} -
-
- -
-
- @for (installedApp of environment.installedApps; track installedApp) { - {{ installedApp }} - } + +
+ +
+
+ @for (installedApp of environment.installedApps; track installedApp) { + {{ installedApp }} + } +
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 b88e5168..46c7100e 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 @@ -32,6 +32,10 @@ import { UserAvatarComponent } from '@app/components/user-avatar/user-avatar.com import { EnvironmentStatusInfoComponent } from '../environment-status-info/environment-status-info.component'; import { EnvironmentStatusTagComponent } from '../environment-status-tag/environment-status-tag.component'; import { DeploymentStepperComponent } from '../deployment-stepper/deployment-stepper.component'; +import { ToggleButtonModule } from 'primeng/togglebutton'; +import { FormsModule } from '@angular/forms'; +import { ToggleSwitchModule } from 'primeng/toggleswitch'; + @Component({ selector: 'app-environment-list-view', imports: [ @@ -54,6 +58,9 @@ import { DeploymentStepperComponent } from '../deployment-stepper/deployment-ste CommonModule, TimeAgoPipe, UserAvatarComponent, + ToggleButtonModule, + FormsModule, + ToggleSwitchModule, ], providers: [DatePipe], templateUrl: './environment-list-view.component.html', @@ -67,6 +74,8 @@ export class EnvironmentListViewComponent implements OnDestroy { private currentTime = signal(Date.now()); private intervalId: number | undefined; + showLatestDeployment: boolean = true; + editable = input(); deployable = input(); hideLinkToList = input(); @@ -226,4 +235,13 @@ export class EnvironmentListViewComponent implements OnDestroy { return timeLeftMinutes > 1 ? `You can unlock this environment in ${timeLeftMinutes} minutes` : 'You can unlock this environment in 1 minute'; } } + + isDeploymentOngoing(environment: EnvironmentDto) { + if (!environment.latestDeployment) { + return false; + } else if (environment.latestDeployment.state && ['SUCCESS', 'FAILURE', 'ERROR', 'INACTIVE', 'UNKNOWN'].includes(environment.latestDeployment.state)) { + return false; + } + return true; + } } diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java index 16361b57..abf93ad0 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java @@ -123,7 +123,7 @@ public LatestDeploymentUnion findLatestDeployment(Environment env) { if (latestHeliosOpt.isPresent() && latestDeploymentOpt.isPresent()) { HeliosDeployment latestHelios = latestHeliosOpt.get(); Deployment latestDeployment = latestDeploymentOpt.get(); - + // TODO: add logs and check what's returned in ehre // Compare updatedAt timestamps to determine the latest if (latestDeployment.getCreatedAt().isAfter(latestHelios.getCreatedAt())) { return LatestDeploymentUnion.realDeployment(latestDeployment, latestHelios.getCreatedAt()); diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java index beb2f4ed..4afdbb3a 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java @@ -14,7 +14,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; import jakarta.persistence.Table; import java.time.OffsetDateTime; import java.util.Map; @@ -86,10 +85,10 @@ protected void onCreate() { updatedAt = OffsetDateTime.now(); } - @PreUpdate - protected void onUpdate() { - updatedAt = OffsetDateTime.now(); - } + // @PreUpdate + // protected void onUpdate() { + // updatedAt = OffsetDateTime.now(); + // } // Enum to represent deployment status public enum Status { diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java index 8a3c2c65..a60d4580 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java @@ -16,6 +16,7 @@ import jakarta.transaction.Transactional; import java.io.IOException; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; @@ -207,24 +208,35 @@ private void processRunForHeliosDeployment(GHWorkflowRun workflowRun) throws IOE // triggered the workflow run // Then we can check whether it's triggered via Helios-App or a Github User via Github UI. // We only need to update heliosDeployment if it's triggered via Helios-App - heliosDeploymentRepository .findTopByBranchNameAndCreatedAtLessThanEqualOrderByCreatedAtDesc( workflowRun.getHeadBranch(), DateUtil.convertToOffsetDateTime(workflowRun.getRunStartedAt())) .ifPresent( heliosDeployment -> { - HeliosDeployment.Status mappedStatus = - mapWorkflowRunStatus(workflowRun.getStatus(), workflowRun.getConclusion()); - log.debug("Mapped status {} to {}", workflowRun.getStatus(), mappedStatus); - - // Update the deployment status - heliosDeployment.setStatus(mappedStatus); - log.info( - "Updated HeliosDeployment {} to status {}", - heliosDeployment.getId(), - mappedStatus); - heliosDeploymentRepository.save(heliosDeployment); + try { + if (workflowRun + .getUpdatedAt() + .toInstant() + .isAfter(heliosDeployment.getUpdatedAt().toInstant())) { + heliosDeployment.setUpdatedAt( + OffsetDateTime.ofInstant( + workflowRun.getUpdatedAt().toInstant(), ZoneId.systemDefault())); + HeliosDeployment.Status mappedStatus = + mapWorkflowRunStatus(workflowRun.getStatus(), workflowRun.getConclusion()); + log.debug("Mapped status {} to {}", workflowRun.getStatus(), mappedStatus); + + // Update the deployment status + heliosDeployment.setStatus(mappedStatus); + log.info( + "Updated HeliosDeployment {} to status {}", + heliosDeployment.getId(), + mappedStatus); + heliosDeploymentRepository.save(heliosDeployment); + } + } catch (IOException e) { + e.printStackTrace(); + } }); } } From 2a599a4c644553e2453713be803140d61ba8ba50 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:54:10 +0100 Subject: [PATCH 06/14] helios udpate logic changed --- .../github/GitHubDeploymentSyncService.java | 26 +++++++++++-------- .../github/GitHubWorkflowRunSyncService.java | 4 +-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/github/GitHubDeploymentSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/github/GitHubDeploymentSyncService.java index 94976ee4..8058256c 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/github/GitHubDeploymentSyncService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/github/GitHubDeploymentSyncService.java @@ -62,11 +62,10 @@ public GitHubDeploymentSyncService( * Synchronizes deployments for all repositories. * * @param repositories the list of GitHub repositories to sync deployments from - * @param since an optional timestamp to fetch deployments since + * @param since an optional timestamp to fetch deployments since */ public void syncDeploymentsOfAllRepositories( - @NotNull List repositories, - Optional since) { + @NotNull List repositories, Optional since) { repositories.forEach(ghRepository -> syncDeploymentsOfRepository(ghRepository, since)); } @@ -74,11 +73,10 @@ public void syncDeploymentsOfAllRepositories( * Synchronizes deployments for a specific repository. * * @param ghRepository the GitHub repository to sync deployments from - * @param since an optional timestamp to fetch deployments since + * @param since an optional timestamp to fetch deployments since */ public void syncDeploymentsOfRepository( - @NotNull GHRepository ghRepository, - Optional since) { + @NotNull GHRepository ghRepository, Optional since) { try { // Fetch the GitRepository entity String fullName = ghRepository.getFullName(); @@ -106,8 +104,8 @@ public void syncDeploymentsOfRepository( * Synchronizes deployments for a specific environment. * * @param ghRepository the GitHub repository - * @param environment the environment entity - * @param since an optional timestamp to fetch deployments since + * @param environment the environment entity + * @param since an optional timestamp to fetch deployments since */ public void syncDeploymentsOfEnvironment( @NotNull GHRepository ghRepository, @@ -171,9 +169,9 @@ public void syncDeploymentsOfEnvironment( * repository. * * @param deploymentSource the source (GHDeployment or GitHubDeploymentDto) wrapped as a - * DeploymentSource - * @param gitRepository the associated GitRepository entity - * @param environment the associated environment entity + * DeploymentSource + * @param gitRepository the associated GitRepository entity + * @param environment the associated environment entity */ @Transactional void processDeployment( @@ -227,6 +225,12 @@ private void updateHeliosDeployment(Deployment deployment, Environment environme heliosDeployment.setDeploymentId(deployment.getId()); heliosDeployment.setStatus( HeliosDeployment.mapDeploymentStateToHeliosStatus(deployment.getState())); + if (deployment + .getUpdatedAt() + .toInstant() + .isAfter(heliosDeployment.getUpdatedAt().toInstant())) { + heliosDeployment.setUpdatedAt(deployment.getUpdatedAt()); + } heliosDeploymentRepository.save(heliosDeployment); log.info("Helios Deployment updated"); } diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java index a60d4580..a726e88f 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/workflow/github/GitHubWorkflowRunSyncService.java @@ -16,7 +16,6 @@ import jakarta.transaction.Transactional; import java.io.IOException; import java.time.OffsetDateTime; -import java.time.ZoneId; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; @@ -220,8 +219,7 @@ private void processRunForHeliosDeployment(GHWorkflowRun workflowRun) throws IOE .toInstant() .isAfter(heliosDeployment.getUpdatedAt().toInstant())) { heliosDeployment.setUpdatedAt( - OffsetDateTime.ofInstant( - workflowRun.getUpdatedAt().toInstant(), ZoneId.systemDefault())); + DateUtil.convertToOffsetDateTime(workflowRun.getUpdatedAt())); HeliosDeployment.Status mappedStatus = mapWorkflowRunStatus(workflowRun.getStatus(), workflowRun.getConclusion()); log.debug("Mapped status {} to {}", workflowRun.getStatus(), mappedStatus); From 229a7df9263a5b21aaf5541cc25cc5604e70ed6c Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 03:03:33 +0100 Subject: [PATCH 07/14] latest deployment check logic udpate --- .../de/tum/cit/aet/helios/environment/EnvironmentService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java index abf93ad0..bc662bb1 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/environment/EnvironmentService.java @@ -125,7 +125,8 @@ public LatestDeploymentUnion findLatestDeployment(Environment env) { Deployment latestDeployment = latestDeploymentOpt.get(); // TODO: add logs and check what's returned in ehre // Compare updatedAt timestamps to determine the latest - if (latestDeployment.getCreatedAt().isAfter(latestHelios.getCreatedAt())) { + if (latestDeployment.getCreatedAt().isAfter(latestHelios.getCreatedAt()) + || latestDeployment.getCreatedAt().isEqual(latestHelios.getCreatedAt())) { return LatestDeploymentUnion.realDeployment(latestDeployment, latestHelios.getCreatedAt()); } else { return LatestDeploymentUnion.heliosDeployment(latestHelios); From ce41e49a3aa15e10b074bd954a525ff49157bb46 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:37:06 +0100 Subject: [PATCH 08/14] progress bar size change --- .../deployment-stepper/deployment-stepper.component.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html index 2f23f7ed..bc9d56ce 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.html @@ -44,8 +44,10 @@ }
- - +
+ + +
From b94ab550ad51ac5bf11f27c9fa3a1720ee3d8978 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:40:26 +0100 Subject: [PATCH 09/14] different time estimation in pr and branch --- .../deployment-stepper.component.ts | 9 +++++---- client/src/app/core/modules/openapi/schemas.gen.ts | 3 +++ client/src/app/core/modules/openapi/types.gen.ts | 1 + server/application-server/openapi.yaml | 2 ++ .../aet/helios/deployment/DeploymentService.java | 13 ++++++++++++- .../helios/deployment/LatestDeploymentUnion.java | 14 ++++++++++++++ .../cit/aet/helios/environment/EnvironmentDto.java | 2 ++ .../helios/heliosdeployment/HeliosDeployment.java | 5 +++++ .../migration/V8__add_pr_to_helios_deployment.sql | 8 ++++++++ 9 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 server/application-server/src/main/resources/db/migration/V8__add_pr_to_helios_deployment.sql diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts index f09d06fb..a2583f83 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts @@ -33,7 +33,7 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { estimatedTimes = { QUEUED: 5, WAITING: 1, - PENDING: 1, + PENDING: 5, IN_PROGRESS: 5, }; @@ -45,6 +45,10 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { this.intervalId = window.setInterval(() => { this.time = Date.now(); }, 1000); + + if (this.deployment?.prName !== null && this.deployment?.prName !== undefined) { + this.estimatedTimes.PENDING = 1; + } } ngOnDestroy(): void { @@ -117,13 +121,10 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { const elapsedForSegmentMs = elapsedMs - cumulativeEstimateMs > 0 ? elapsedMs - cumulativeEstimateMs : 0; let ratio = elapsedForSegmentMs / estimatedMs; - console.log('elapsed for segment ', elapsedForSegmentMs); - console.log('estimated ms ', estimatedMs); if (ratio > 1) { ratio = 1; } progress = segStart + ratio * 33; - console.log('progress ', progress); return Math.floor(progress); } diff --git a/client/src/app/core/modules/openapi/schemas.gen.ts b/client/src/app/core/modules/openapi/schemas.gen.ts index c07f50f0..7289e4ed 100644 --- a/client/src/app/core/modules/openapi/schemas.gen.ts +++ b/client/src/app/core/modules/openapi/schemas.gen.ts @@ -86,6 +86,9 @@ export const EnvironmentDeploymentSchema = { releaseCandidateName: { type: 'string', }, + prName: { + type: 'string', + }, user: { $ref: '#/components/schemas/UserInfoDto', }, diff --git a/client/src/app/core/modules/openapi/types.gen.ts b/client/src/app/core/modules/openapi/types.gen.ts index e103b166..fdcc7848 100644 --- a/client/src/app/core/modules/openapi/types.gen.ts +++ b/client/src/app/core/modules/openapi/types.gen.ts @@ -27,6 +27,7 @@ export type EnvironmentDeployment = { ref?: string; task?: string; releaseCandidateName?: string; + prName?: string; user?: UserInfoDto; createdAt?: string; updatedAt?: string; diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index a201584d..130b3e5e 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -905,6 +905,8 @@ components: type: string releaseCandidateName: type: string + prName: + type: string user: $ref: "#/components/schemas/UserInfoDto" createdAt: diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/DeploymentService.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/DeploymentService.java index c038642f..bb253956 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/DeploymentService.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/deployment/DeploymentService.java @@ -11,6 +11,8 @@ import de.tum.cit.aet.helios.github.GitHubService; import de.tum.cit.aet.helios.heliosdeployment.HeliosDeployment; import de.tum.cit.aet.helios.heliosdeployment.HeliosDeploymentRepository; +import de.tum.cit.aet.helios.pullrequest.PullRequest; +import de.tum.cit.aet.helios.pullrequest.PullRequestRepository; import de.tum.cit.aet.helios.user.User; import de.tum.cit.aet.helios.workflow.Workflow; import de.tum.cit.aet.helios.workflow.WorkflowService; @@ -41,6 +43,7 @@ public class DeploymentService { private final EnvironmentLockHistoryRepository lockHistoryRepository; private final EnvironmentRepository environmentRepository; private final BranchService branchService; + private final PullRequestRepository pullRequestRepository; public DeploymentService( DeploymentRepository deploymentRepository, @@ -51,7 +54,8 @@ public DeploymentService( HeliosDeploymentRepository heliosDeploymentRepository, BranchService branchService, EnvironmentLockHistoryRepository lockHistoryRepository, - EnvironmentRepository environmentRepository) { + EnvironmentRepository environmentRepository, + PullRequestRepository pullRequestRepository) { this.deploymentRepository = deploymentRepository; this.gitHubService = gitHubService; this.environmentService = environmentService; @@ -61,6 +65,7 @@ public DeploymentService( this.lockHistoryRepository = lockHistoryRepository; this.environmentRepository = environmentRepository; this.branchService = branchService; + this.pullRequestRepository = pullRequestRepository; } public Optional getDeploymentById(Long id) { @@ -125,6 +130,11 @@ public void deployToEnvironment(DeployRequest deployRequest) { throw new DeploymentException("Deployment is still in progress, please wait."); } + // Set the PR associated with the deployment + Optional optionalPullRequest = + pullRequestRepository.findByRepositoryRepositoryIdAndHeadRefNameOrHeadSha( + RepositoryContext.getRepositoryId(), deployRequest.branchName(), branchCommitSha); + User githubUser = this.authService.getUserFromGithubId(); // Create a new HeliosDeployment record HeliosDeployment heliosDeployment = new HeliosDeployment(); @@ -134,6 +144,7 @@ public void deployToEnvironment(DeployRequest deployRequest) { heliosDeployment.setBranchName(deployRequest.branchName()); heliosDeployment.setSha(branchCommitSha); heliosDeployment.setCreator(githubUser); + heliosDeployment.setPullRequest(optionalPullRequest.orElse(null)); heliosDeployment = heliosDeploymentRepository.saveAndFlush(heliosDeployment); // Build parameters for the workflow 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 3f46bd88..ccb2c871 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 @@ -157,6 +157,20 @@ public OffsetDateTime getUpdatedAt() { } } + public String getPullRequestName() { + if (isRealDeployment()) { + return realDeployment.getPullRequest() != null + ? realDeployment.getPullRequest().getTitle() + : null; + } else if (isHeliosDeployment()) { + return heliosDeployment.getPullRequest() != null + ? heliosDeployment.getPullRequest().getTitle() + : 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 ae521a7c..3ddf4dba 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 @@ -67,6 +67,7 @@ public static record EnvironmentDeployment( String ref, String task, String releaseCandidateName, + String prName, UserInfoDto user, OffsetDateTime createdAt, OffsetDateTime updatedAt) { @@ -85,6 +86,7 @@ public static EnvironmentDeployment fromUnion( .findByRepositoryRepositoryIdAndCommitSha(union.getRepository().id(), union.getSha()) .map(ReleaseCandidate::getName) .orElse(null), + union.getPullRequestName(), UserInfoDto.fromUser(union.getCreator()), union.getCreatedAt(), union.getUpdatedAt()); diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java index 4afdbb3a..afb7b821 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java @@ -2,6 +2,7 @@ import de.tum.cit.aet.helios.deployment.Deployment; import de.tum.cit.aet.helios.environment.Environment; +import de.tum.cit.aet.helios.pullrequest.PullRequest; import de.tum.cit.aet.helios.user.User; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -85,6 +86,10 @@ protected void onCreate() { updatedAt = OffsetDateTime.now(); } + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pull_request_id") + private PullRequest pullRequest; + // @PreUpdate // protected void onUpdate() { // updatedAt = OffsetDateTime.now(); diff --git a/server/application-server/src/main/resources/db/migration/V8__add_pr_to_helios_deployment.sql b/server/application-server/src/main/resources/db/migration/V8__add_pr_to_helios_deployment.sql new file mode 100644 index 00000000..a261d537 --- /dev/null +++ b/server/application-server/src/main/resources/db/migration/V8__add_pr_to_helios_deployment.sql @@ -0,0 +1,8 @@ +-- Add pull_request_id column to the helios_deployment table +ALTER TABLE helios_deployment + ADD COLUMN pull_request_id BIGINT; + +-- Add foreign key constraint to ensure referential integrity +ALTER TABLE helios_deployment + ADD CONSTRAINT fk_helios_deployment_pull_request + FOREIGN KEY (pull_request_id) REFERENCES public.issue(id); From 8ddd99ebcd1c8935e04716b8a52badedf8deabbf Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:32:17 +0100 Subject: [PATCH 10/14] migrations script name change --- ...helios_deployment.sql => V13__add_pr_to_helios_deployment.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/application-server/src/main/resources/db/migration/{V12__add_pr_to_helios_deployment.sql => V13__add_pr_to_helios_deployment.sql} (100%) diff --git a/server/application-server/src/main/resources/db/migration/V12__add_pr_to_helios_deployment.sql b/server/application-server/src/main/resources/db/migration/V13__add_pr_to_helios_deployment.sql similarity index 100% rename from server/application-server/src/main/resources/db/migration/V12__add_pr_to_helios_deployment.sql rename to server/application-server/src/main/resources/db/migration/V13__add_pr_to_helios_deployment.sql From 355ab4b5ecf746b99648085fa78aa38c008ac00b Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:39:30 +0100 Subject: [PATCH 11/14] comment deleted --- .../cit/aet/helios/heliosdeployment/HeliosDeployment.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java b/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java index 5dc3d3de..ac6fd2c4 100644 --- a/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java +++ b/server/application-server/src/main/java/de/tum/cit/aet/helios/heliosdeployment/HeliosDeployment.java @@ -93,11 +93,6 @@ protected void onCreate() { @JoinColumn(name = "pull_request_id") private PullRequest pullRequest; - // @PreUpdate - // protected void onUpdate() { - // updatedAt = OffsetDateTime.now(); - // } - // Enum to represent deployment status public enum Status { /** Deployment called and waiting GitHub webhook listener. */ From 8987bab8b45cba76e5f4a88dc4e049e9fc904d65 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:39:50 +0100 Subject: [PATCH 12/14] waiting state changed to requested state --- .../deployment-state-tag.component.ts | 8 +- .../deployment-stepper.component.ts | 90 +++++++++++-------- .../app/core/modules/openapi/schemas.gen.ts | 2 +- .../src/app/core/modules/openapi/types.gen.ts | 2 +- server/application-server/openapi.yaml | 1 + .../deployment/LatestDeploymentUnion.java | 49 ++++++++-- .../helios/environment/EnvironmentDto.java | 3 +- 7 files changed, 106 insertions(+), 49 deletions(-) diff --git a/client/src/app/components/environments/deployment-state-tag/deployment-state-tag.component.ts b/client/src/app/components/environments/deployment-state-tag/deployment-state-tag.component.ts index 30b95a90..8f47255d 100644 --- a/client/src/app/components/environments/deployment-state-tag/deployment-state-tag.component.ts +++ b/client/src/app/components/environments/deployment-state-tag/deployment-state-tag.component.ts @@ -2,9 +2,9 @@ import { Component, computed, input } from '@angular/core'; import { TagModule } from 'primeng/tag'; import { IconsModule } from 'icons.module'; import { TooltipModule } from 'primeng/tooltip'; -import { DeploymentDto } from '@app/core/modules/openapi'; +import { EnvironmentDeployment } from '@app/core/modules/openapi'; -type BaseDeploymentState = NonNullable; +type BaseDeploymentState = NonNullable; type ExtendedDeploymentState = BaseDeploymentState | 'NEVER_DEPLOYED' | 'REPLACED'; @Component({ @@ -34,6 +34,7 @@ export class DeploymentStateTagComponent { UNKNOWN: 'secondary', NEVER_DEPLOYED: 'secondary', REPLACED: 'contrast', + REQUESTED: 'warn', }; return severityMap[this.internalState()]; }); @@ -51,6 +52,7 @@ export class DeploymentStateTagComponent { UNKNOWN: 'question-mark', NEVER_DEPLOYED: 'question-mark', REPLACED: 'repeat', + REQUESTED: 'progress', }; return iconMap[this.internalState()]; }); @@ -73,6 +75,7 @@ export class DeploymentStateTagComponent { UNKNOWN: 'unknown', NEVER_DEPLOYED: 'never deployed', REPLACED: 'replaced', + REQUESTED: 'requested', }; return valueMap[this.internalState()]; }); @@ -90,6 +93,7 @@ export class DeploymentStateTagComponent { UNKNOWN: 'Deployment state unknown', NEVER_DEPLOYED: 'Never deployed', REPLACED: 'Deployment was replaced', + REQUESTED: 'Deployment requested', }; return tooltipMap[this.internalState()]; }); diff --git a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts index a2583f83..e4c3fb9d 100644 --- a/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts +++ b/client/src/app/components/environments/deployment-stepper/deployment-stepper.component.ts @@ -1,25 +1,42 @@ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, computed, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IconsModule } from 'icons.module'; import { ProgressBarModule } from 'primeng/progressbar'; import { EnvironmentDeployment } from '@app/core/modules/openapi'; import { TooltipModule } from 'primeng/tooltip'; +interface EstimatedTimes { + REQUESTED: number; + PENDING: number; + IN_PROGRESS: number; +} + @Component({ selector: 'app-deployment-stepper', imports: [CommonModule, IconsModule, ProgressBarModule, TooltipModule], templateUrl: './deployment-stepper.component.html', }) export class DeploymentStepperComponent implements OnInit, OnDestroy { - @Input() deployment: EnvironmentDeployment | undefined; + // Create a private signal to hold the deployment value. + private _deployment = signal(undefined); + + @Input() + set deployment(value: EnvironmentDeployment | undefined) { + this._deployment.set(value); + } + get deployment(): EnvironmentDeployment | undefined { + return this._deployment(); + } // Define the four steps. (Note: estimatedTimes are defined only for the first three.) - steps: ('WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS')[] = ['WAITING', 'PENDING', 'IN_PROGRESS', 'SUCCESS']; + steps: ('REQUESTED' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS')[] = ['REQUESTED', 'PENDING', 'IN_PROGRESS', 'SUCCESS']; + + // Mapping of state keys to their display descriptions. stepDescriptions: { - [key in 'QUEUED' | 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS' | 'ERROR' | 'FAILURE' | 'UNKNOWN' | 'INACTIVE']: string; + [key in 'QUEUED' | 'REQUESTED' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS' | 'ERROR' | 'FAILURE' | 'UNKNOWN' | 'INACTIVE']: string; } = { QUEUED: 'Deployment Queued', - WAITING: 'Request sent to Github', + REQUESTED: 'Request sent to Github', PENDING: 'Preparing deployment', IN_PROGRESS: 'Deployment in progress', SUCCESS: 'Deployment successful', @@ -30,12 +47,15 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { }; // Estimated times in minutes for each non-terminal step. - estimatedTimes = { - QUEUED: 5, - WAITING: 1, - PENDING: 5, - IN_PROGRESS: 5, - }; + // Define estimatedTimes as a computed signal that depends on the reactive deployment signal. + estimatedTimes = computed(() => { + const deployment = this._deployment(); + return { + REQUESTED: 1, + PENDING: deployment?.prName != null ? 1 : 10, // if deployment started from PR then no build time it's 1 minute, if there is a build via branch then it's 4-7 minute in avg + IN_PROGRESS: 4, // deployment state takes around 1-3 mintues + }; + }); // A timer updated every second to drive the dynamic progress. time: number = Date.now(); @@ -45,10 +65,6 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { this.intervalId = window.setInterval(() => { this.time = Date.now(); }, 1000); - - if (this.deployment?.prName !== null && this.deployment?.prName !== undefined) { - this.estimatedTimes.PENDING = 1; - } } ngOnDestroy(): void { @@ -64,17 +80,20 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { */ get currentEffectiveStepIndex(): number { if (!this.deployment || !this.deployment.createdAt) return 0; - // if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { - const index = this.steps.indexOf(this.deployment.state as 'WAITING' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS'); + const index = this.steps.indexOf(this.deployment.state as 'REQUESTED' | 'PENDING' | 'IN_PROGRESS' | 'SUCCESS'); return index !== -1 ? index : 3; - // } - // return this.virtualStepIndex; } + /** + * Checks if the deployment is in an error state. + */ isErrorState(): boolean { return ['ERROR', 'FAILURE'].includes(this.deployment?.state || ''); } + /** + * Checks if the deployment is in an unknown state. + */ isUnknownState(): boolean { return ['UNKNOWN', 'INACTIVE'].includes(this.deployment?.state || ''); } @@ -87,44 +106,38 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { */ getStepStatus(index: number): string { const effectiveStep = this.currentEffectiveStepIndex; - if (this.isUnknownState()) return 'unknown'; // all steps should be unkown - if (index < effectiveStep) return 'completed'; //it's already done - if (index === effectiveStep) return this.isErrorState() ? 'error' : this.steps[index] == 'SUCCESS' ? 'completed' : 'active'; + if (this.isUnknownState()) return 'unknown'; + if (index < effectiveStep) return 'completed'; + if (index === effectiveStep) return this.isErrorState() ? 'error' : this.steps[index] === 'SUCCESS' ? 'completed' : 'active'; return 'upcoming'; } /** * Computes overall progress in a piecewise manner. - * Each segment (WAITING, PENDING, IN_PROGRESS) is allotted 25% of the bar. + * Each segment (REQUESTED, PENDING, IN_PROGRESS) is allotted 25% of the bar. * For the current segment, progress is calculated as a ratio of its elapsed time; * if a segment is already completed (i.e. virtualStepIndex > segment index), that segment is full. * When the virtual step reaches 3, the progress returns 100%. */ get dynamicProgress(): number { if (!this.deployment || !this.deployment.createdAt) return 0; - // If the server indicates a terminal state, show full progress. if (['UNKNOWN', 'INACTIVE'].includes(this.deployment.state || '')) { return 0; } else if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { return 100; } - const start = new Date(this.deployment.createdAt).getTime(); // start time is when the deployment was created - const elapsedMs = this.time - start > 0 ? this.time - start : 0; // elapsed time - // If the virtual step is 3, show 100%. + const start = new Date(this.deployment.createdAt).getTime(); + const elapsedMs = this.time - start > 0 ? this.time - start : 0; if (this.currentEffectiveStepIndex === 3) return 100; - let progress = 0; - let cumulativeEstimateMs = 0; - // There are three segments: 0 (0–25%), 1 (25–50%), 2 (50–75%). const segStart = this.currentEffectiveStepIndex * 33; - const estimatedMs = this.estimatedTimes[this.steps[this.currentEffectiveStepIndex] as keyof typeof this.estimatedTimes] * 60000; - - const elapsedForSegmentMs = elapsedMs - cumulativeEstimateMs > 0 ? elapsedMs - cumulativeEstimateMs : 0; + const estimatedMs = this.estimatedTimes()[this.steps[this.currentEffectiveStepIndex] as keyof EstimatedTimes] * 60000; + const elapsedForSegmentMs = elapsedMs > 0 ? elapsedMs : 0; let ratio = elapsedForSegmentMs / estimatedMs; if (ratio > 1) { ratio = 1; } - progress = segStart + ratio * 33; + const progress = segStart + ratio * 33; return Math.floor(progress); } @@ -138,7 +151,7 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { const elapsedMinutes = (this.time - start) / 60000; let cumulative = 0; for (let i = 0; i < 3; i++) { - const est = this.estimatedTimes[this.steps[i] as keyof typeof this.estimatedTimes]; + const est = this.estimatedTimes()[this.steps[i] as keyof EstimatedTimes]; if (this.getStepStatus(i) !== 'completed') { cumulative += est; } @@ -155,10 +168,13 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { return ''; } + /** + * Returns a display name for a given step. + */ getStepDisplayName(step: string): string { return ( { - WAITING: 'REQUESTED', + REQUESTED: 'REQUESTED', PENDING: 'PRE-DEPLOYMENT', IN_PROGRESS: 'DEPLOYING', SUCCESS: 'SUCCESS', @@ -173,12 +189,10 @@ export class DeploymentStepperComponent implements OnInit, OnDestroy { getDeploymentDuration(): string { if (!this.deployment || !this.deployment.createdAt) return ''; - // For terminal states, use finishedAt if available; otherwise use current time. let endTime: number; if (['SUCCESS', 'ERROR', 'FAILURE'].includes(this.deployment.state || '')) { endTime = this.deployment.updatedAt ? new Date(this.deployment.updatedAt).getTime() : this.time; } else { - // For ongoing deployments, we use the current time. endTime = this.time; } diff --git a/client/src/app/core/modules/openapi/schemas.gen.ts b/client/src/app/core/modules/openapi/schemas.gen.ts index 1d8e46d0..c4133d2f 100644 --- a/client/src/app/core/modules/openapi/schemas.gen.ts +++ b/client/src/app/core/modules/openapi/schemas.gen.ts @@ -69,7 +69,7 @@ export const EnvironmentDeploymentSchema = { }, state: { type: 'string', - enum: ['PENDING', 'WAITING', 'SUCCESS', 'ERROR', 'FAILURE', 'IN_PROGRESS', 'QUEUED', 'INACTIVE', 'UNKNOWN'], + enum: ['REQUESTED', 'PENDING', 'WAITING', 'SUCCESS', 'ERROR', 'FAILURE', 'IN_PROGRESS', 'QUEUED', 'INACTIVE', 'UNKNOWN'], }, statusesUrl: { type: 'string', diff --git a/client/src/app/core/modules/openapi/types.gen.ts b/client/src/app/core/modules/openapi/types.gen.ts index a87446e5..11f09a95 100644 --- a/client/src/app/core/modules/openapi/types.gen.ts +++ b/client/src/app/core/modules/openapi/types.gen.ts @@ -21,7 +21,7 @@ export type WorkflowMembershipDto = { export type EnvironmentDeployment = { id: number; url?: string; - state?: 'PENDING' | 'WAITING' | 'SUCCESS' | 'ERROR' | 'FAILURE' | 'IN_PROGRESS' | 'QUEUED' | 'INACTIVE' | 'UNKNOWN'; + state?: 'REQUESTED' | 'PENDING' | 'WAITING' | 'SUCCESS' | 'ERROR' | 'FAILURE' | 'IN_PROGRESS' | 'QUEUED' | 'INACTIVE' | 'UNKNOWN'; statusesUrl?: string; sha?: string; ref?: string; diff --git a/server/application-server/openapi.yaml b/server/application-server/openapi.yaml index 721ca636..7de2be72 100644 --- a/server/application-server/openapi.yaml +++ b/server/application-server/openapi.yaml @@ -917,6 +917,7 @@ components: state: type: string enum: + - REQUESTED - PENDING - WAITING - SUCCESS 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 6da926f7..072e031c 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 @@ -111,18 +111,57 @@ public Environment getEnvironment() { } } - public Deployment.State getState() { + public State getState() { if (isRealDeployment()) { - return realDeployment.getState(); + return State.fromDeploymentState(realDeployment.getState()); } else if (isHeliosDeployment()) { - Deployment.State state = - HeliosDeployment.mapHeliosStatusToDeploymentState(heliosDeployment.getStatus()); - return state; + return State.fromHeliosStatus(heliosDeployment.getStatus()); } else { return null; } } + public static enum State { + REQUESTED, + + // Deployment.State + PENDING, + WAITING, + SUCCESS, + ERROR, + FAILURE, + IN_PROGRESS, + QUEUED, + INACTIVE, + UNKNOWN; + + public static State fromDeploymentState(Deployment.State state) { + return switch (state) { + case PENDING -> PENDING; + case WAITING -> WAITING; + case SUCCESS -> SUCCESS; + case ERROR -> ERROR; + case FAILURE -> FAILURE; + case IN_PROGRESS -> IN_PROGRESS; + case QUEUED -> QUEUED; + case INACTIVE -> INACTIVE; + case UNKNOWN -> UNKNOWN; + default -> throw new IllegalArgumentException("Invalid state: " + state); + }; + } + + public static State fromHeliosStatus(HeliosDeployment.Status status) { + return switch (status) { + case WAITING -> REQUESTED; + case QUEUED -> PENDING; + case IN_PROGRESS -> IN_PROGRESS; + case DEPLOYMENT_SUCCESS -> SUCCESS; + case FAILED -> FAILURE; + case IO_ERROR, UNKNOWN -> UNKNOWN; + }; + } + } + public String getStatusesUrl() { if (isRealDeployment()) { return realDeployment.getStatusesUrl(); 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 4122e228..38283c28 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 @@ -1,6 +1,5 @@ package de.tum.cit.aet.helios.environment; -import de.tum.cit.aet.helios.deployment.Deployment; import de.tum.cit.aet.helios.deployment.LatestDeploymentUnion; import de.tum.cit.aet.helios.deployment.LatestDeploymentUnion.DeploymentType; import de.tum.cit.aet.helios.environment.status.EnvironmentStatus; @@ -63,7 +62,7 @@ public static EnvironmentStatusDto fromEnvironmentStatus(EnvironmentStatus envir public static record EnvironmentDeployment( @NonNull Long id, String url, - Deployment.State state, + LatestDeploymentUnion.State state, String statusesUrl, String sha, String ref, From 9c3048b19490ec5bdeed5f32b82b8183398b0ce6 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:57:12 +0100 Subject: [PATCH 13/14] migration for cascade --- .../migration/V14__add_missing_on_delete_cascade.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql diff --git a/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql b/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql new file mode 100644 index 00000000..92bf1a58 --- /dev/null +++ b/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql @@ -0,0 +1,10 @@ +------------------------------------------------------------------------------- +-- workflow_group_membership -> workflow +------------------------------------------------------------------------------- +-- Removing a workflow_group_membership row automatically removes any workflow rows that reference that release_candidate. +ALTER TABLE public.workflow_group_membership DROP CONSTRAINT fkixdlgaqu4hykyehs17gbvyvfj; +ALTER TABLE public.workflow_group_membership + ADD CONSTRAINT fkixdlgaqu4hykyehs17gbvyvfj + FOREIGN KEY (repository_id) + REFERENCES public.workflow(repository_id) + ON DELETE CASCADE; \ No newline at end of file From f690c104e15154e2b5f9fd1356222a7c8ba52377 Mon Sep 17 00:00:00 2001 From: Turker Koc <39654393+TurkerKoc@users.noreply.github.com> Date: Tue, 18 Feb 2025 00:16:42 +0100 Subject: [PATCH 14/14] migration cascade fix --- .../db/migration/V14__add_missing_on_delete_cascade.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql b/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql index 92bf1a58..64e26e6d 100644 --- a/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql +++ b/server/application-server/src/main/resources/db/migration/V14__add_missing_on_delete_cascade.sql @@ -5,6 +5,6 @@ ALTER TABLE public.workflow_group_membership DROP CONSTRAINT fkixdlgaqu4hykyehs17gbvyvfj; ALTER TABLE public.workflow_group_membership ADD CONSTRAINT fkixdlgaqu4hykyehs17gbvyvfj - FOREIGN KEY (repository_id) - REFERENCES public.workflow(repository_id) + FOREIGN KEY (workflow_id) + REFERENCES public.workflow(id) ON DELETE CASCADE; \ No newline at end of file