Skip to content

Commit 5078ff6

Browse files
authored
feat: improve confirmation dialogs (#741)
1 parent 229bbb5 commit 5078ff6

18 files changed

+502
-55
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<p-dialog [(visible)]="visible" [modal]="true" [style]="{ width: '26rem' }" draggable="false" (onHide)="onCancel()">
2+
<ng-template #header>
3+
<div class="font-bold text-xl flex items-center gap-2">
4+
<i-tabler name="cloud-upload" class="!size-5 flex-shrink-0" />
5+
Deploy
6+
</div>
7+
</ng-template>
8+
9+
<!-- Loading State -->
10+
@if (query.isPending()) {
11+
<ng-container>
12+
<div class="p-4 space-y-3">
13+
<p-skeleton width="60%" height="1.5rem"></p-skeleton>
14+
<p-skeleton width="100%" height="1rem"></p-skeleton>
15+
<p-skeleton width="100%" height="1rem"></p-skeleton>
16+
<p-skeleton width="30%" height="2rem"></p-skeleton>
17+
</div>
18+
</ng-container>
19+
} @else {
20+
<div class="pt-2">
21+
<!-- Environment banner -->
22+
<div
23+
class="p-4 rounded flex items-start space-x-3"
24+
[ngClass]="{
25+
'bg-red-50 text-red-800 ring-1 ring-red-300': environment().type === 'PRODUCTION',
26+
'bg-amber-50 text-amber-800 ring-1 ring-amber-300': environment().type === 'STAGING',
27+
}"
28+
role="alert"
29+
>
30+
<!-- Tabler icon -->
31+
@if (environment().type === 'PRODUCTION') {
32+
<i-tabler name="alert-triangle" class="!size-6 shrink-0 text-red-600" />
33+
} @else if (environment().type === 'STAGING') {
34+
<i-tabler name="info-circle" class="!size-6 shrink-0 text-amber-600" />
35+
} @else {
36+
<i-tabler name="server" class="!size-5 shrink-0 text-slate-600" />
37+
}
38+
39+
<div class="space-y-1 text-sm">
40+
@if (environment().type === 'PRODUCTION') {
41+
<p>
42+
You are about to deploy to production <span class="font-semibold italic">{{ environmentName() }}</span
43+
>.
44+
</p>
45+
<br />
46+
<p>Double‑check everything before you continue.</p>
47+
} @else if (environment().type === 'STAGING') {
48+
<p>
49+
You are about to deploy to staging <span class="font-semibold italic">{{ environmentName() }}</span
50+
>.
51+
</p>
52+
} @else {
53+
<p>
54+
You are about to deploy to test server <span class="font-semibold italic">{{ environmentName() }}</span
55+
>.
56+
</p>
57+
}
58+
</div>
59+
</div>
60+
61+
<!-- Reviewers -->
62+
@if (hasReviewers()) {
63+
<div class="mt-4 text-sm space-y-2">
64+
<p>
65+
<strong>Required reviewers ({{ reviewers().length }}):</strong>
66+
<span>
67+
{{ reviewersLine() }}
68+
</span>
69+
</p>
70+
<p class="text-gray-700 dark:text-gray-300 text-xs italic">
71+
Helios will attempt to auto-approve this deployment. If you or your team aren’t in the required reviewers list, the pipeline pauses just before the deploy step and
72+
waits for one of the reviewers to approve.
73+
</p>
74+
</div>
75+
<br />
76+
}
77+
78+
<!-- Repository confirmation -->
79+
@if (environment().repository?.nameWithOwner) {
80+
<div class="mt-4 text-sm space-y-1">
81+
<p>
82+
To confirm, type <code class="font-semibold">{{ environment().repository?.nameWithOwner }}</code> below
83+
</p>
84+
<input
85+
pInputText
86+
class="w-full border p-2 rounded"
87+
type="text"
88+
(input)="onRepoInput($event)"
89+
placeholder="{{ environment().repository?.nameWithOwner }}"
90+
autocomplete="off"
91+
/>
92+
</div>
93+
}
94+
</div>
95+
}
96+
97+
<ng-template pTemplate="footer">
98+
<div class="flex justify-between w-full">
99+
<button pButton type="button" label="Cancel" class="p-button-text" (click)="onCancel()"></button>
100+
101+
<button
102+
pButton
103+
type="button"
104+
label="Deploy"
105+
class="ml-2"
106+
[disabled]="query.isPending() || (environment().repository?.nameWithOwner && repoConfirm !== environment().repository?.nameWithOwner)"
107+
(click)="onDeploy()"
108+
></button>
109+
</div>
110+
</ng-template>
111+
</p-dialog>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Component, computed, input, model, output } from '@angular/core';
2+
import { EnvironmentDto, EnvironmentReviewersDto } from '@app/core/modules/openapi';
3+
import { getEnvironmentReviewersOptions } from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
4+
import { injectQuery } from '@tanstack/angular-query-experimental';
5+
import { SkeletonModule } from 'primeng/skeleton';
6+
import { DialogModule } from 'primeng/dialog';
7+
import { NgClass } from '@angular/common';
8+
import { PrimeTemplate } from 'primeng/api';
9+
import { ButtonModule } from 'primeng/button';
10+
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
11+
import { IconAlertTriangle, IconInfoCircle, IconServer, IconCloudUpload } from 'angular-tabler-icons/icons';
12+
import { InputText } from 'primeng/inputtext';
13+
14+
@Component({
15+
selector: 'app-deploy-confirmation',
16+
imports: [SkeletonModule, DialogModule, ButtonModule, NgClass, PrimeTemplate, TablerIconComponent, InputText],
17+
providers: [
18+
provideTablerIcons({
19+
IconAlertTriangle,
20+
IconInfoCircle,
21+
IconServer,
22+
IconCloudUpload,
23+
}),
24+
],
25+
templateUrl: './deploy-confirmation.component.html',
26+
})
27+
export class DeployConfirmationComponent {
28+
/** Input text for the confirmation */
29+
repoConfirm = '';
30+
31+
/** Two-way bind this from the parent */
32+
isVisible = model.required<boolean>();
33+
/** The environment to deploy */
34+
environment = input.required<EnvironmentDto>();
35+
environmentName = computed(() => (this.environment().displayName?.trim() ? this.environment().displayName : (this.environment().name ?? '')));
36+
37+
/** Emits true if Deploy clicked, false if Cancel */
38+
confirmed = output<boolean>();
39+
40+
// Fetch Reviewers
41+
query = injectQuery(() => ({
42+
...getEnvironmentReviewersOptions({
43+
path: { environmentId: this.environment().id },
44+
}),
45+
enabled: !!this.environment().id,
46+
throwOnError: false,
47+
retry: false,
48+
}));
49+
50+
// derived data
51+
reviewers = computed(() => (this.query.data() as EnvironmentReviewersDto)?.reviewers ?? []);
52+
hasReviewers = computed(() => this.reviewers().length > 0);
53+
reviewersLine = computed(() => {
54+
if (!this.hasReviewers()) {
55+
return '';
56+
}
57+
58+
return this.reviewers()
59+
.map(r => {
60+
const name = r.name || r.login;
61+
return r.type !== 'User' ? `${name} (Team)` : name;
62+
})
63+
.join(', ');
64+
});
65+
66+
onRepoInput(event: Event) {
67+
this.repoConfirm = (event.target as HTMLInputElement).value;
68+
}
69+
70+
onCancel() {
71+
this.isVisible.update(() => false);
72+
this.confirmed.emit(false);
73+
}
74+
75+
onDeploy() {
76+
this.isVisible.update(() => false);
77+
this.confirmed.emit(true);
78+
}
79+
80+
get visible(): boolean {
81+
return this.isVisible();
82+
}
83+
84+
set visible(val: boolean) {
85+
this.isVisible.set(val);
86+
}
87+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<p-dialog [(visible)]="isVisible" [modal]="true" [style]="{ width: '26rem' }" draggable="false" (onHide)="onCancel()">
2+
<ng-template #header>
3+
<div class="font-bold text-xl flex items-center gap-2">
4+
<i-tabler name="lock" class="!size-5 flex-shrink-0" />
5+
Lock Environment
6+
</div>
7+
</ng-template>
8+
9+
<div class="text-sm p-4 space-y-3">
10+
<p>
11+
Are you sure you want to lock <b>{{ environmentName() }}</b
12+
>?
13+
</p>
14+
</div>
15+
16+
<ng-template #footer>
17+
<div class="flex justify-between w-full">
18+
<p-button label="Cancel" [text]="true" (click)="onCancel()" />
19+
<p-button label="Lock" severity="primary" (click)="onLock()" />
20+
</div>
21+
</ng-template>
22+
</p-dialog>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Component, computed, input, model, output } from '@angular/core';
2+
import { EnvironmentDto } from '@app/core/modules/openapi';
3+
import { Button } from 'primeng/button';
4+
import { Dialog } from 'primeng/dialog';
5+
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
6+
import { IconLock } from 'angular-tabler-icons/icons';
7+
8+
@Component({
9+
selector: 'app-lock-confirmation',
10+
imports: [Button, Dialog, TablerIconComponent],
11+
providers: [
12+
provideTablerIcons({
13+
IconLock,
14+
}),
15+
],
16+
templateUrl: './lock-confirmation.component.html',
17+
})
18+
export class LockConfirmationComponent {
19+
/** Two-way bind this from the parent */
20+
isVisible = model.required<boolean>();
21+
/** The environment to deploy */
22+
environment = input.required<EnvironmentDto>();
23+
environmentName = computed(() => (this.environment().displayName?.trim() ? this.environment().displayName : (this.environment().name ?? '')));
24+
25+
/** Emits true if lock clicked, false if Cancel */
26+
confirmed = output<boolean>();
27+
28+
onCancel() {
29+
this.isVisible.update(() => false);
30+
this.confirmed.emit(false);
31+
}
32+
33+
onLock() {
34+
this.isVisible.update(() => false);
35+
this.confirmed.emit(true);
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<p-dialog [(visible)]="isVisible" [modal]="true" [style]="{ width: '26rem' }" draggable="false" (onHide)="onCancel()">
2+
<ng-template #header>
3+
<div class="font-bold text-xl flex items-center gap-2">
4+
<i-tabler name="upload" class="!size-5 flex-shrink-0" />
5+
Publish Release Candidate
6+
</div>
7+
</ng-template>
8+
9+
<div class="text-sm p-4 space-y-3">
10+
<p>
11+
Are you sure you want to publish release candidate <b>{{ releaseName() }}</b> as a <b>draft</b> to GitHub? This action can only be undone directly in GitHub.
12+
</p>
13+
</div>
14+
15+
<ng-template #footer>
16+
<div class="flex justify-between w-full">
17+
<p-button label="Cancel" [text]="true" (click)="onCancel()" />
18+
<p-button label="Publish" severity="primary" (click)="onPublish()" />
19+
</div>
20+
</ng-template>
21+
</p-dialog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Component, input, model, output } from '@angular/core';
2+
import { Dialog } from 'primeng/dialog';
3+
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
4+
import { Button } from 'primeng/button';
5+
import { IconUpload } from 'angular-tabler-icons/icons';
6+
7+
@Component({
8+
selector: 'app-publish-draft-release-confirmation',
9+
imports: [Dialog, TablerIconComponent, Button],
10+
providers: [
11+
provideTablerIcons({
12+
IconUpload,
13+
}),
14+
],
15+
templateUrl: './publish-draft-release-confirmation.component.html',
16+
})
17+
export class PublishDraftReleaseConfirmationComponent {
18+
isVisible = model.required<boolean>();
19+
releaseName = input.required<string>();
20+
21+
/** Emits true if publish clicked, false if Cancel */
22+
confirmed = output<boolean>();
23+
24+
onCancel() {
25+
this.isVisible.update(() => false);
26+
this.confirmed.emit(false);
27+
}
28+
29+
onPublish() {
30+
this.isVisible.update(() => false);
31+
this.confirmed.emit(true);
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<p-dialog [(visible)]="isVisible" [modal]="true" [style]="{ width: '28rem' }" draggable="false" (onHide)="onCancel()">
2+
<ng-template #header>
3+
<div class="font-bold text-xl flex items-center gap-2">
4+
<i-tabler name="key" class="!size-5 flex-shrink-0" />
5+
Generate secret
6+
</div>
7+
</ng-template>
8+
9+
<div class="flex flex-col gap-4 text-sm pt-2">
10+
<div class="flex gap-3 p-4 bg-red-50 dark:bg-red-900/40 ring-1 ring-red-200 dark:ring-red-700 rounded">
11+
<i-tabler name="alert-triangle" class="!size-6 shrink-0 text-red-600 dark:text-red-300" />
12+
<p>
13+
You are about to <span class="font-semibold">generate the secret key</span>. The current secret will <span class="font-semibold">stop working immediately</span>. Make sure
14+
to update your CI/CD secret store right after generating a new one. It is displayed <b>only once</b>—copy it immediately.
15+
</p>
16+
</div>
17+
</div>
18+
19+
<ng-template #footer>
20+
<div class="flex justify-between w-full">
21+
<p-button label="Cancel" [text]="true" (click)="onCancel()" />
22+
<p-button label="Generate" severity="danger" (click)="onGenerate()" />
23+
</div>
24+
</ng-template>
25+
</p-dialog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Component, model, output } from '@angular/core';
2+
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
3+
import { Dialog } from 'primeng/dialog';
4+
import { Button } from 'primeng/button';
5+
import { IconKey, IconAlertTriangle } from 'angular-tabler-icons/icons';
6+
7+
@Component({
8+
selector: 'app-secret-generate-confirmation',
9+
imports: [TablerIconComponent, Dialog, Button],
10+
providers: [
11+
provideTablerIcons({
12+
IconKey,
13+
IconAlertTriangle,
14+
}),
15+
],
16+
templateUrl: './secret-generate-confirmation.component.html',
17+
})
18+
export class SecretGenerateConfirmationComponent {
19+
isVisible = model.required<boolean>();
20+
/** Emits true if generate clicked, false if Cancel */
21+
confirmed = output<boolean>();
22+
23+
onCancel() {
24+
this.isVisible.update(() => false);
25+
this.confirmed.emit(false);
26+
}
27+
28+
onGenerate() {
29+
this.isVisible.update(() => false);
30+
this.confirmed.emit(true);
31+
}
32+
}

client/src/app/components/environments/environment-list/environment-list-view.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,10 @@ <h3 class="text-lg font-semibold mb-2">{{ capitalizeFirstLetter(group[0]) }}</h3
9393
</div>
9494
}
9595
}
96+
@if (deployDialogVisible()) {
97+
<app-deploy-confirmation [(isVisible)]="deployDialogVisible" [environment]="selectedEnv()!" (confirmed)="onDeployDialogConfirmed($event)"></app-deploy-confirmation>
98+
} @else if (lockDialogVisible()) {
99+
<app-lock-confirmation [(isVisible)]="lockDialogVisible" [environment]="selectedEnv()!" (confirmed)="onLockEnvironmentConfirmed($event)"></app-lock-confirmation>
100+
}
96101
}
97102
}

0 commit comments

Comments
 (0)