Skip to content

Commit d319aea

Browse files
committed
Enhance dashboard modals with notification and confirmation features
1 parent 3a42f30 commit d319aea

File tree

1 file changed

+192
-23
lines changed

1 file changed

+192
-23
lines changed

src/Acmebot/wwwroot/dashboard/index.html

Lines changed: 192 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -365,10 +365,20 @@
365365

366366
/* Form controls in modal */
367367
.modal-card-body .input,
368-
.modal-card-body .select select {
368+
.modal-card-body .select select,
369+
.modal-card-body .button {
369370
border-radius: 6px;
370371
border-color: var(--border-color);
371372
font-size: 0.9rem;
373+
height: 2.5em;
374+
}
375+
376+
.modal-card-body .select {
377+
height: 2.5em;
378+
}
379+
380+
.modal-card-body .select select {
381+
height: 100%;
372382
}
373383

374384
.modal-card-body .input:focus,
@@ -654,6 +664,74 @@
654664
td {
655665
vertical-align: middle !important;
656666
}
667+
668+
/* Notification / Confirm modal */
669+
.notify-modal .modal-card {
670+
max-width: 480px;
671+
}
672+
673+
.notify-modal .modal-card-head {
674+
padding: 1rem 1.25rem;
675+
}
676+
677+
.notify-modal .modal-card-body {
678+
padding: 1.5rem;
679+
display: flex;
680+
align-items: flex-start;
681+
gap: 1rem;
682+
}
683+
684+
.notify-icon {
685+
flex-shrink: 0;
686+
width: 40px;
687+
height: 40px;
688+
border-radius: 50%;
689+
display: flex;
690+
align-items: center;
691+
justify-content: center;
692+
font-size: 1.1rem;
693+
}
694+
695+
.notify-icon.is-success {
696+
background: #e6f4e6;
697+
color: var(--success);
698+
}
699+
700+
.notify-icon.is-danger {
701+
background: #fde7e9;
702+
color: var(--danger);
703+
}
704+
705+
.notify-icon.is-warning {
706+
background: #fff4ce;
707+
color: #9a6700;
708+
}
709+
710+
.notify-body {
711+
flex: 1;
712+
min-width: 0;
713+
}
714+
715+
.notify-title {
716+
font-weight: 600;
717+
font-size: 0.95rem;
718+
margin-bottom: 0.35rem;
719+
}
720+
721+
.notify-message {
722+
font-size: 0.88rem;
723+
color: var(--text-secondary);
724+
line-height: 1.5;
725+
word-break: break-word;
726+
}
727+
728+
.notify-modal .modal-card-foot {
729+
padding: 0.75rem 1.25rem;
730+
}
731+
732+
.notify-modal .modal-card-foot .button {
733+
min-width: 80px;
734+
}
657735
</style>
658736
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js" integrity="sha512-Tn2m0TIpgVyTzzvmxLNuqbSJH3JP8jm+Cy3hvHrW7ndTDcJ1w5mBiksqDBb8GpE2ksktFvDB/ykZ0mDpsZj20w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
659737
</head>
@@ -764,6 +842,12 @@
764842
<p>No certificates match "{{ searchQuery }}"</p>
765843
</div>
766844
</div>
845+
<div class="cert-card" v-else-if="loading">
846+
<div class="empty-state">
847+
<div class="icon"><i class="fas fa-spinner fa-spin"></i></div>
848+
<p>Loading certificates...</p>
849+
</div>
850+
</div>
767851
<div class="cert-card" v-else-if="!loading && managedCertificates.length === 0">
768852
<div class="empty-state">
769853
<div class="icon"><i class="fas fa-certificate"></i></div>
@@ -874,7 +958,7 @@
874958
</div>
875959
<!-- Add certificate modal -->
876960
<div class="modal" :class="{ 'is-active': add.modalActive }">
877-
<div class="modal-background"></div>
961+
<div class="modal-background" @click="add.sending || (add.modalActive = false)"></div>
878962
<div class="modal-card">
879963
<header class="modal-card-head">
880964
<p class="modal-card-title"><i class="fas fa-plus-circle" style="margin-right:0.5rem;opacity:0.8"></i>Add Certificate</p>
@@ -908,7 +992,7 @@
908992
<div class="field-body">
909993
<div class="field has-addons">
910994
<p class="control">
911-
<input v-model="add.recordName" class="input" type="text" placeholder="Record name" :disabled="add.zone === null">
995+
<input v-model="add.recordName" class="input" type="text" placeholder="Record name" :disabled="add.zone === null" @keyup.enter="addDnsName">
912996
</p>
913997
<p class="control">
914998
<a class="button is-static">
@@ -1062,7 +1146,7 @@
10621146
</div>
10631147
<!-- Details certificate modal -->
10641148
<div class="modal" :class="{ 'is-active': details.modalActive }">
1065-
<div class="modal-background"></div>
1149+
<div class="modal-background" @click="details.sending || (details.modalActive = false)"></div>
10661150
<div class="modal-card">
10671151
<header class="modal-card-head">
10681152
<p class="modal-card-title"><i class="fas fa-info-circle" style="margin-right:0.5rem;opacity:0.8"></i>Certificate Details</p>
@@ -1109,7 +1193,8 @@
11091193
</div>
11101194
<div class="field-body">
11111195
<div class="content">
1112-
{{ formatExpiresOn(details.certificate) }}
1196+
{{ formatCreatedOn(details.certificate.expiresOn) }}
1197+
<span :class="statusClass(details.certificate)" class="status-badge" style="margin-left:0.5rem"><span class="status-dot"></span>{{ formatExpiresOn(details.certificate) }}</span>
11131198
</div>
11141199
</div>
11151200
</div>
@@ -1170,7 +1255,7 @@
11701255
</div>
11711256
<div class="field-body">
11721257
<div class="content">
1173-
{{ details.certificate.reuseKey }}
1258+
{{ details.certificate.reuseKey ? 'Yes' : 'No' }}
11741259
</div>
11751260
</div>
11761261
</div>
@@ -1180,7 +1265,7 @@
11801265
</div>
11811266
<div class="field-body">
11821267
<div class="content">
1183-
{{ details.certificate.isIssuedByAcmebot }}
1268+
{{ details.certificate.isIssuedByAcmebot ? 'Yes' : 'No' }}
11841269
</div>
11851270
</div>
11861271
</div>
@@ -1223,6 +1308,51 @@
12231308
</footer>
12241309
</div>
12251310
</div>
1311+
<!-- Notification modal -->
1312+
<div class="modal notify-modal" :class="{ 'is-active': notify.active }">
1313+
<div class="modal-background" @click="closeNotify"></div>
1314+
<div class="modal-card">
1315+
<header class="modal-card-head">
1316+
<p class="modal-card-title">
1317+
<i class="fas" :class="notify.titleIcon" style="margin-right:0.5rem;opacity:0.8"></i>{{ notify.title }}
1318+
</p>
1319+
</header>
1320+
<section class="modal-card-body">
1321+
<div class="notify-icon" :class="notify.iconClass">
1322+
<i class="fas" :class="notify.icon"></i>
1323+
</div>
1324+
<div class="notify-body">
1325+
<div class="notify-message">{{ notify.message }}</div>
1326+
</div>
1327+
</section>
1328+
<footer class="modal-card-foot is-justify-content-flex-end">
1329+
<button class="button is-primary" @click="closeNotify">OK</button>
1330+
</footer>
1331+
</div>
1332+
</div>
1333+
<!-- Confirm modal -->
1334+
<div class="modal notify-modal" :class="{ 'is-active': confirmDialog.active }">
1335+
<div class="modal-background" @click="cancelConfirm"></div>
1336+
<div class="modal-card">
1337+
<header class="modal-card-head">
1338+
<p class="modal-card-title">
1339+
<i class="fas fa-exclamation-triangle" style="margin-right:0.5rem;opacity:0.8"></i>Confirm
1340+
</p>
1341+
</header>
1342+
<section class="modal-card-body">
1343+
<div class="notify-icon is-warning">
1344+
<i class="fas fa-question"></i>
1345+
</div>
1346+
<div class="notify-body">
1347+
<div class="notify-message">{{ confirmDialog.message }}</div>
1348+
</div>
1349+
</section>
1350+
<footer class="modal-card-foot is-justify-content-flex-end">
1351+
<button class="button is-danger" @click="acceptConfirm">{{ confirmDialog.okLabel }}</button>
1352+
<button class="button" @click="cancelConfirm">Cancel</button>
1353+
</footer>
1354+
</div>
1355+
</div>
12261356
</div>
12271357
</div>
12281358
</section>
@@ -1267,6 +1397,21 @@
12671397
certificate: "",
12681398
sending: false,
12691399
modalActive: false
1400+
},
1401+
notify: {
1402+
active: false,
1403+
title: "",
1404+
titleIcon: "fa-info-circle",
1405+
message: "",
1406+
icon: "fa-check-circle",
1407+
iconClass: "is-success",
1408+
_resolve: null
1409+
},
1410+
confirmDialog: {
1411+
active: false,
1412+
message: "",
1413+
okLabel: "OK",
1414+
_resolve: null
12701415
}
12711416
};
12721417
},
@@ -1278,7 +1423,7 @@
12781423
return this.certificates.filter(x => x.isIssuedByAcmebot && !x.isSameEndpoint);
12791424
},
12801425
unmanagedCertificates() {
1281-
return this.certificates.filter(x => !x.isIssuedByAcmebot && !x.isIssuedByAcmebot);
1426+
return this.certificates.filter(x => !x.isIssuedByAcmebot);
12821427
},
12831428
groupedManagedCertificates() {
12841429
return this.groupByZone(this.managedCertificates);
@@ -1405,7 +1550,7 @@
14051550
}
14061551

14071552
if (this.add.dnsProviderName !== "" && this.add.dnsProviderName !== this.add.zone.dnsProviderName) {
1408-
alert("DNS zones belonging to different DNS Providers cannot be included in the same certificate.");
1553+
this.showNotify("DNS zones belonging to different DNS Providers cannot be included in the same certificate.", { type: 'warning' });
14091554
return;
14101555
}
14111556

@@ -1452,7 +1597,6 @@
14521597
response = await axios.get(response.headers["location"]);
14531598

14541599
if (response.status === 200) {
1455-
alert("The certificate was successfully issued.");
14561600
break;
14571601
}
14581602
}
@@ -1463,6 +1607,7 @@
14631607
this.add.sending = false;
14641608
this.add.modalActive = false;
14651609

1610+
await this.showNotify("The certificate was successfully issued.");
14661611
await this.refresh();
14671612
},
14681613
async openAdd() {
@@ -1491,7 +1636,6 @@
14911636
response = await axios.get(response.headers["location"]);
14921637

14931638
if (response.status === 200) {
1494-
alert("The certificate was successfully renewed.");
14951639
break;
14961640
}
14971641
}
@@ -1502,27 +1646,52 @@
15021646
this.details.sending = false;
15031647
this.details.modalActive = false;
15041648

1649+
await this.showNotify("The certificate was successfully renewed.");
15051650
await this.refresh();
15061651
},
15071652
async revokeCertificate() {
1508-
if (!confirm(`${this.details.certificate.name} revoke your certificate. Are you sure?`)) {
1509-
return;
1510-
}
1653+
const ok = await this.showConfirm(`Are you sure you want to revoke the certificate "${this.details.certificate.name}"?`, { okLabel: 'Revoke' });
1654+
if (!ok) return;
15111655

15121656
this.details.sending = true;
15131657

15141658
try {
1515-
const response = await axios.post(`/api/certificate/${this.details.certificate.name}/revoke`);
1516-
1517-
if (response.status === 200) {
1518-
alert("The certificate was successfully revoked.");
1519-
}
1659+
await axios.post(`/api/certificate/${this.details.certificate.name}/revoke`);
15201660
} catch (error) {
15211661
this.handleHttpError(error);
15221662
}
15231663

15241664
this.details.sending = false;
15251665
this.details.modalActive = false;
1666+
1667+
await this.showNotify("The certificate was successfully revoked.");
1668+
},
1669+
showNotify(message, { type = 'success' } = {}) {
1670+
const config = {
1671+
success: { title: 'Success', titleIcon: 'fa-check-circle', icon: 'fa-check-circle', iconClass: 'is-success' },
1672+
error: { title: 'Error', titleIcon: 'fa-exclamation-circle', icon: 'fa-times-circle', iconClass: 'is-danger' },
1673+
warning: { title: 'Warning', titleIcon: 'fa-exclamation-triangle', icon: 'fa-exclamation-triangle', iconClass: 'is-warning' }
1674+
}[type] || { title: 'Notice', titleIcon: 'fa-info-circle', icon: 'fa-info-circle', iconClass: 'is-success' };
1675+
return new Promise(resolve => {
1676+
Object.assign(this.notify, { active: true, message, ...config, _resolve: resolve });
1677+
});
1678+
},
1679+
closeNotify() {
1680+
this.notify.active = false;
1681+
if (this.notify._resolve) this.notify._resolve();
1682+
},
1683+
showConfirm(message, { okLabel = 'OK' } = {}) {
1684+
return new Promise(resolve => {
1685+
Object.assign(this.confirmDialog, { active: true, message, okLabel, _resolve: resolve });
1686+
});
1687+
},
1688+
acceptConfirm() {
1689+
this.confirmDialog.active = false;
1690+
if (this.confirmDialog._resolve) this.confirmDialog._resolve(true);
1691+
},
1692+
cancelConfirm() {
1693+
this.confirmDialog.active = false;
1694+
if (this.confirmDialog._resolve) this.confirmDialog._resolve(false);
15261695
},
15271696
toUnicode(value) {
15281697
return punycode.toUnicode(value);
@@ -1559,7 +1728,7 @@
15591728
if (this.isShortLived(certificate)) {
15601729
if (remainHours < 24) return `${remainHours}h remaining`;
15611730
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
1562-
const hours = Math.round((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
1731+
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
15631732
return `${days}d ${hours}h remaining`;
15641733
}
15651734

@@ -1582,13 +1751,13 @@
15821751
errors.push(problem.errors[key][0]);
15831752
}
15841753

1585-
alert(errors.join("\n"));
1754+
this.showNotify(errors.join("\n"), { type: 'error' });
15861755
} else {
15871756
const message = problem.detail ?? problem.output;
15881757
if (message) {
1589-
alert(message);
1758+
this.showNotify(message, { type: 'error' });
15901759
} else {
1591-
alert(`HTTP Response ${error.response.status} Error`);
1760+
this.showNotify(`HTTP Response ${error.response.status} Error`, { type: 'error' });
15921761
}
15931762
}
15941763
}

0 commit comments

Comments
 (0)