|
365 | 365 |
|
366 | 366 | /* Form controls in modal */ |
367 | 367 | .modal-card-body .input, |
368 | | - .modal-card-body .select select { |
| 368 | + .modal-card-body .select select, |
| 369 | + .modal-card-body .button { |
369 | 370 | border-radius: 6px; |
370 | 371 | border-color: var(--border-color); |
371 | 372 | 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%; |
372 | 382 | } |
373 | 383 |
|
374 | 384 | .modal-card-body .input:focus, |
|
654 | 664 | td { |
655 | 665 | vertical-align: middle !important; |
656 | 666 | } |
| 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 | + } |
657 | 735 | </style> |
658 | 736 | <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> |
659 | 737 | </head> |
|
764 | 842 | <p>No certificates match "{{ searchQuery }}"</p> |
765 | 843 | </div> |
766 | 844 | </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> |
767 | 851 | <div class="cert-card" v-else-if="!loading && managedCertificates.length === 0"> |
768 | 852 | <div class="empty-state"> |
769 | 853 | <div class="icon"><i class="fas fa-certificate"></i></div> |
|
874 | 958 | </div> |
875 | 959 | <!-- Add certificate modal --> |
876 | 960 | <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> |
878 | 962 | <div class="modal-card"> |
879 | 963 | <header class="modal-card-head"> |
880 | 964 | <p class="modal-card-title"><i class="fas fa-plus-circle" style="margin-right:0.5rem;opacity:0.8"></i>Add Certificate</p> |
|
908 | 992 | <div class="field-body"> |
909 | 993 | <div class="field has-addons"> |
910 | 994 | <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"> |
912 | 996 | </p> |
913 | 997 | <p class="control"> |
914 | 998 | <a class="button is-static"> |
|
1062 | 1146 | </div> |
1063 | 1147 | <!-- Details certificate modal --> |
1064 | 1148 | <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> |
1066 | 1150 | <div class="modal-card"> |
1067 | 1151 | <header class="modal-card-head"> |
1068 | 1152 | <p class="modal-card-title"><i class="fas fa-info-circle" style="margin-right:0.5rem;opacity:0.8"></i>Certificate Details</p> |
|
1109 | 1193 | </div> |
1110 | 1194 | <div class="field-body"> |
1111 | 1195 | <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> |
1113 | 1198 | </div> |
1114 | 1199 | </div> |
1115 | 1200 | </div> |
|
1170 | 1255 | </div> |
1171 | 1256 | <div class="field-body"> |
1172 | 1257 | <div class="content"> |
1173 | | - {{ details.certificate.reuseKey }} |
| 1258 | + {{ details.certificate.reuseKey ? 'Yes' : 'No' }} |
1174 | 1259 | </div> |
1175 | 1260 | </div> |
1176 | 1261 | </div> |
|
1180 | 1265 | </div> |
1181 | 1266 | <div class="field-body"> |
1182 | 1267 | <div class="content"> |
1183 | | - {{ details.certificate.isIssuedByAcmebot }} |
| 1268 | + {{ details.certificate.isIssuedByAcmebot ? 'Yes' : 'No' }} |
1184 | 1269 | </div> |
1185 | 1270 | </div> |
1186 | 1271 | </div> |
|
1223 | 1308 | </footer> |
1224 | 1309 | </div> |
1225 | 1310 | </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> |
1226 | 1356 | </div> |
1227 | 1357 | </div> |
1228 | 1358 | </section> |
|
1267 | 1397 | certificate: "", |
1268 | 1398 | sending: false, |
1269 | 1399 | 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 |
1270 | 1415 | } |
1271 | 1416 | }; |
1272 | 1417 | }, |
|
1278 | 1423 | return this.certificates.filter(x => x.isIssuedByAcmebot && !x.isSameEndpoint); |
1279 | 1424 | }, |
1280 | 1425 | unmanagedCertificates() { |
1281 | | - return this.certificates.filter(x => !x.isIssuedByAcmebot && !x.isIssuedByAcmebot); |
| 1426 | + return this.certificates.filter(x => !x.isIssuedByAcmebot); |
1282 | 1427 | }, |
1283 | 1428 | groupedManagedCertificates() { |
1284 | 1429 | return this.groupByZone(this.managedCertificates); |
|
1405 | 1550 | } |
1406 | 1551 |
|
1407 | 1552 | 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' }); |
1409 | 1554 | return; |
1410 | 1555 | } |
1411 | 1556 |
|
|
1452 | 1597 | response = await axios.get(response.headers["location"]); |
1453 | 1598 |
|
1454 | 1599 | if (response.status === 200) { |
1455 | | - alert("The certificate was successfully issued."); |
1456 | 1600 | break; |
1457 | 1601 | } |
1458 | 1602 | } |
|
1463 | 1607 | this.add.sending = false; |
1464 | 1608 | this.add.modalActive = false; |
1465 | 1609 |
|
| 1610 | + await this.showNotify("The certificate was successfully issued."); |
1466 | 1611 | await this.refresh(); |
1467 | 1612 | }, |
1468 | 1613 | async openAdd() { |
|
1491 | 1636 | response = await axios.get(response.headers["location"]); |
1492 | 1637 |
|
1493 | 1638 | if (response.status === 200) { |
1494 | | - alert("The certificate was successfully renewed."); |
1495 | 1639 | break; |
1496 | 1640 | } |
1497 | 1641 | } |
|
1502 | 1646 | this.details.sending = false; |
1503 | 1647 | this.details.modalActive = false; |
1504 | 1648 |
|
| 1649 | + await this.showNotify("The certificate was successfully renewed."); |
1505 | 1650 | await this.refresh(); |
1506 | 1651 | }, |
1507 | 1652 | 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; |
1511 | 1655 |
|
1512 | 1656 | this.details.sending = true; |
1513 | 1657 |
|
1514 | 1658 | 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`); |
1520 | 1660 | } catch (error) { |
1521 | 1661 | this.handleHttpError(error); |
1522 | 1662 | } |
1523 | 1663 |
|
1524 | 1664 | this.details.sending = false; |
1525 | 1665 | 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); |
1526 | 1695 | }, |
1527 | 1696 | toUnicode(value) { |
1528 | 1697 | return punycode.toUnicode(value); |
|
1559 | 1728 | if (this.isShortLived(certificate)) { |
1560 | 1729 | if (remainHours < 24) return `${remainHours}h remaining`; |
1561 | 1730 | 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)); |
1563 | 1732 | return `${days}d ${hours}h remaining`; |
1564 | 1733 | } |
1565 | 1734 |
|
|
1582 | 1751 | errors.push(problem.errors[key][0]); |
1583 | 1752 | } |
1584 | 1753 |
|
1585 | | - alert(errors.join("\n")); |
| 1754 | + this.showNotify(errors.join("\n"), { type: 'error' }); |
1586 | 1755 | } else { |
1587 | 1756 | const message = problem.detail ?? problem.output; |
1588 | 1757 | if (message) { |
1589 | | - alert(message); |
| 1758 | + this.showNotify(message, { type: 'error' }); |
1590 | 1759 | } else { |
1591 | | - alert(`HTTP Response ${error.response.status} Error`); |
| 1760 | + this.showNotify(`HTTP Response ${error.response.status} Error`, { type: 'error' }); |
1592 | 1761 | } |
1593 | 1762 | } |
1594 | 1763 | } |
|
0 commit comments