diff --git a/core/ui/public/i18n/en/translation.json b/core/ui/public/i18n/en/translation.json index 8330ac4fc2..56414baaa5 100644 --- a/core/ui/public/i18n/en/translation.json +++ b/core/ui/public/i18n/en/translation.json @@ -848,6 +848,7 @@ }, "settings_tls_certificates": { "title": "TLS certificates", + "certificates": "Certificates", "show_all": "Show all", "request_certificate": "Request certificate", "filter_certificates": "Filter certificates", @@ -889,6 +890,7 @@ "uploaded": "Uploaded", "automatic": "Automatic", "obsolete": "Obsolete", + "obsolete_tooltip": "No longer referenced by any application, HTTP route, or user request. It may still be reachable by clients until renewed or deleted.", "valid": "Valid", "expiring": "Expiring", "expired": "Expired", @@ -931,16 +933,20 @@ "certificates_not_displayed": "Certificates configured on {instanceId} are not displayed" }, "settings_acme_servers": { - "title": "ACME servers", - "search_acme_server": "Search ACME server", - "no_acme_server": "No ACME server", - "no_acme_server_description": "There is no ACME server configured", - "url": "URL", + "acme_settings": "ACME settings", + "search_acme_settings": "Search", + "no_acme_settings": "No ACME settings", + "no_acme_settings_description": "No cluster node has ACME settings configured", + "acme_directory_url": "ACME directory URL", + "acme_directory_url_helper": "URL of the ACME provider used to issue certificates.", "node": "Node", - "edit_acme_server": "Edit ACME server", - "edit_acme_server_for_node": "Edit ACME server for {node}", - "reload_page": "Page reload needed", - "edit_acme_server_leader_node_warning": "You are editing the ACME server for the leader node. After you click 'Edit ACME server' you'll need to reload the page" + "challenge": "Challenge type", + "challenge_http_01": "HTTP-01", + "challenge_tls_alpn_01": "TLS-ALPN-01", + "edit_acme_settings": "Edit ACME settings", + "acme_settings_for_node": "ACME settings for {node}", + "acme_restart_message": "HTTP clients on {node} will be briefly disconnected. The connection should resume automatically — otherwise, reload the page manually.", + "save_result_unknown": "Traefik restarted before the result was known. Check the ACME settings of the node and try again if needed." }, "apps_lets_encrypt": { "lets_encrypt_tips": "Requests a Let's Encrypt TLS certificate automatically. Disabling this option deletes the existing certificate (if any). You can also view or delete it in Cluster settings > TLS certificates.", diff --git a/core/ui/src/components/settings/AcmeSettingsPanel.vue b/core/ui/src/components/settings/AcmeSettingsPanel.vue new file mode 100644 index 0000000000..6f66af03de --- /dev/null +++ b/core/ui/src/components/settings/AcmeSettingsPanel.vue @@ -0,0 +1,304 @@ + + + + diff --git a/core/ui/src/components/settings/EditAcmeServerModal.vue b/core/ui/src/components/settings/EditAcmeServerModal.vue index dcf73e0c80..e3e9f0d6e5 100644 --- a/core/ui/src/components/settings/EditAcmeServerModal.vue +++ b/core/ui/src/components/settings/EditAcmeServerModal.vue @@ -12,29 +12,69 @@ @primary-click="setAcmeServer" > - + + + diff --git a/core/ui/src/router/index.js b/core/ui/src/router/index.js index 1679cfc836..babfacab14 100644 --- a/core/ui/src/router/index.js +++ b/core/ui/src/router/index.js @@ -78,11 +78,10 @@ const routes = [ }, { path: "/settings/acme-servers", - name: "SettingsAcmeServers", - component: () => - import( - /* webpackChunkName: "settings-acme-servers" */ "../views/settings/SettingsAcmeServers.vue" - ), + redirect: { + name: "SettingsTlsCertificates", + query: { view: "acme" }, + }, }, { path: "/settings/account", diff --git a/core/ui/src/views/settings/SettingsAcmeServers.vue b/core/ui/src/views/settings/SettingsAcmeServers.vue deleted file mode 100644 index bf1168ffe7..0000000000 --- a/core/ui/src/views/settings/SettingsAcmeServers.vue +++ /dev/null @@ -1,396 +0,0 @@ - - - - - - diff --git a/core/ui/src/views/settings/SettingsTlsCertificates.vue b/core/ui/src/views/settings/SettingsTlsCertificates.vue index d61cce9bd1..77979b0d2a 100644 --- a/core/ui/src/views/settings/SettingsTlsCertificates.vue +++ b/core/ui/src/views/settings/SettingsTlsCertificates.vue @@ -22,21 +22,9 @@ - +

{{ $t("settings_tls_certificates.title") }}

- -
- {{ $t("settings_acme_servers.title") }} - -
-
@@ -52,459 +40,49 @@ - -
- -
-
- -
-
- - {{ $t("settings_tls_certificates.request_certificate") }} - - - {{ $t("settings_tls_certificates.request_certificate") }} - - - - {{ $t("settings_tls_certificates.add_custom_certificate") }} - - - {{ - $t("settings_tls_certificates.delete_obsolete_certificates") - }} - -
-
-
-
- - - - - - - - - {{ $t("common.clear_filters") }} - -
- - - - -
-
-
+ + + + + + + + + + + + + + + +
- - - - - - - - @@ -514,329 +92,113 @@ import { QueryParamService, UtilService, TaskService, - IconService, PageTitleService, - DateTimeService, } from "@nethserver/ns8-ui-lib"; import { mapState } from "vuex"; -import _cloneDeep from "lodash/cloneDeep"; -import RequestTlsCertificateModal from "@/components/settings/RequestTlsCertificateModal.vue"; -import UploadTlsCertificateModal from "@/components/settings/UploadTlsCertificateModal.vue"; -import { StateManager as UploadTlsCertificateState } from "@/components/settings/UploadTlsCertificateModal.vue"; -import Upload20 from "@carbon/icons-vue/es/upload/20"; -import DeleteObsoleteCertificatesModal from "@/components/settings/DeleteObsoleteCertificatesModal.vue"; -import WarningAltFilled16 from "@carbon/icons-vue/es/warning--alt--filled/16"; +import TlsCertificatesPanel from "@/components/settings/TlsCertificatesPanel.vue"; +import AcmeSettingsPanel from "@/components/settings/AcmeSettingsPanel.vue"; + +// order must match the cv-tab order: tabSelected maps the tab index +const TAB_VIEWS = ["certificates", "acme"]; export default { name: "SettingsTlsCertificates", components: { - RequestTlsCertificateModal, - UploadTlsCertificateModal, - DeleteObsoleteCertificatesModal, - WarningAltFilled16, + TlsCertificatesPanel, + AcmeSettingsPanel, }, - mixins: [ - TaskService, - UtilService, - IconService, - QueryParamService, - PageTitleService, - DateTimeService, - ], + mixins: [TaskService, UtilService, QueryParamService, PageTitleService], pageTitle() { return this.$t("settings_tls_certificates.title"); }, data() { return { q: { + view: "", selectedNodeId: "", }, - tablePage: [], - tableColumns: [ - "ui_name", - "ui_type", - "ui_issuer", - "valid_to", - "status", - "names", - "node", - ], - certificatesByTraefikId: {}, + // mount once and keep alive: a v-if would re-run the task chain + acmeTabVisited: false, internalNodes: [], - isShownRequestCertificateModal: false, - isShownDeleteCertificateModal: false, - isShownDeleteObsoleteCertificatesModal: false, - currentErrorAction: "", - currentErrorDescription: "", traefikInstances: [], - currentCertificate: null, - filter: { - text: "", - certificateType: "", - certificateStatus: "", - }, - typeOptions: [ - { - name: "any", - label: this.$t("settings_tls_certificates.any_type"), - value: "any", - }, - { - name: "requested", - label: this.$t("settings_tls_certificates.requested"), - value: "requested", - }, - { - name: "uploaded", - label: this.$t("settings_tls_certificates.uploaded"), - value: "uploaded", - }, - { - name: "automatic", - label: this.$t("settings_tls_certificates.automatic"), - value: "automatic", - }, - { - name: "obsolete", - label: this.$t("settings_tls_certificates.obsolete"), - value: "obsolete", - }, - ], - statusOptions: [ - { - name: "any", - label: this.$t("settings_tls_certificates.any_status"), - value: "any", - }, - { - name: "valid", - label: this.$t("settings_tls_certificates.valid"), - value: "valid", - }, - { - name: "expiring", - label: this.$t("settings_tls_certificates.expiring"), - value: "expiring", - }, - { - name: "expired", - label: this.$t("settings_tls_certificates.expired"), - value: "expired", - }, - ], - Upload20, + taskListeners: [], loading: { listInstalledModules: false, - listCertificatesNum: 0, - deleteCertificate: false, }, error: { listInstalledModules: "", - listCertificates: "", - deleteCertificate: "", }, - listCertificatesErrors: [], - offlineTraefikInstances: [], - uploadTlsCertificateState: new UploadTlsCertificateState(), }; }, computed: { - ...mapState(["clusterNodes"]), - i18nTableColumns() { - return this.tableColumns.map((column) => { - return this.$t("settings_tls_certificates." + column); - }); - }, - loadingCertificates() { - return ( - this.loading.listInstalledModules || - this.loading.listCertificatesNum > 0 - ); - }, - filteredCertificates() { - let certificates = this.certificates; - - // filter by node - if (this.q.selectedNodeId && this.q.selectedNodeId !== "any") { - certificates = certificates.filter((certificate) => { - return certificate.nodeId === this.q.selectedNodeId; - }); - } - - // filter by text - if (this.filter.text.trim()) { - const searchText = this.filter.text.toLowerCase().trim(); - certificates = certificates.filter((cert) => { - return ( - cert.ui_name.toLowerCase().includes(searchText) || - cert.ui_type.toLowerCase().includes(searchText) || - cert.ui_issuer.toLowerCase().includes(searchText) || - cert.expiration.toLowerCase().includes(searchText) || - cert.status.toLowerCase().includes(searchText) || - cert.serial.toLowerCase().includes(searchText) || - cert.names.some((name) => - name.toLowerCase().includes(searchText) - ) || - cert.node.toLowerCase().includes(searchText) - ); - }); - } - - // filter by type - if ( - this.filter.certificateType && - this.filter.certificateType !== "any" - ) { - certificates = certificates.filter((certificate) => { - return certificate.ui_type === this.filter.certificateType; - }); - } - - // filter by status - if ( - this.filter.certificateStatus && - this.filter.certificateStatus !== "any" - ) { - certificates = certificates.filter((certificate) => { - return certificate.status === this.filter.certificateStatus; - }); - } - return certificates; - }, - selectedNodeLabel() { - if (this.q.selectedNodeId === "any" || !this.q.selectedNodeId) { - return ""; - } - - const node = this.internalNodes.find( - (node) => node.value === this.q.selectedNodeId - ); - - if (node) { - return node.label; - } - return ""; - }, - nodesForFilter() { - if (!this.internalNodes.length) { - return []; - } - - // add "Any node" at the beginning of internalNodes array - const nodes = _cloneDeep(this.internalNodes); - - nodes.unshift({ - name: "any", - label: this.$t("common.any_node"), - value: "any", - }); - return nodes; - }, - certificates() { - return Object.values(this.certificatesByTraefikId) - .flat() - .sort(this.sortCertificates); - }, - nodesWithoutCertificate() { - const nodesWithDefaultCert = [ - ...new Set( - this.certificates - .filter((cert) => cert.default && cert.type === "internal") - .map((cert) => cert.nodeId) - ), - ]; - - return this.internalNodes.filter( - (node) => !nodesWithDefaultCert.includes(node.value) - ); - }, - nodesWithObsoleteCertificates() { - const nodesWithObsoleteCert = [ - ...new Set( - this.certificates - .filter((cert) => cert.ui_type === "obsolete") - .map((cert) => cert.nodeId) - ), - ]; - - return this.internalNodes.filter((node) => - nodesWithObsoleteCert.includes(node.value) - ); - }, - hasActiveFilters() { - return ( - (this.filter.text && this.filter.text.trim() !== "") || - (this.filter.certificateType && - this.filter.certificateType !== "any") || - (this.filter.certificateStatus && - this.filter.certificateStatus !== "any") || - (this.q.selectedNodeId && this.q.selectedNodeId !== "any") - ); - }, + ...mapState(["clusterNodes", "isWebsocketConnected"]), }, beforeRouteEnter(to, from, next) { next((vm) => { vm.watchQueryData(vm); vm.queryParamsToDataForCore(vm, to.query); + vm.normalizeView(); }); }, beforeRouteUpdate(to, from, next) { this.queryParamsToDataForCore(this, to.query); + + if (typeof to.query.view === "undefined") { + this.q.view = TAB_VIEWS[0]; + } + this.normalizeView(); next(); }, + watch: { + isWebsocketConnected: function (isConnected) { + if (isConnected) { + this.listInstalledModules(); + } + }, + "q.view": function (view) { + if (view === "acme") { + this.acmeTabVisited = true; + } + }, + }, created() { - // register to events - this.$root.$on("reloadCertificates", this.onReloadCertificates); - this.listInstalledModules(); - - this.$nextTick(() => { - this.filter.certificateType = "any"; - this.filter.certificateStatus = "any"; - }); }, beforeDestroy() { - // remove event listeners - this.$root.$off("reloadCertificates"); + this.clearTaskListeners(); }, methods: { - showRequestCertificateModal() { - this.currentCertificate = null; - this.isShownRequestCertificateModal = true; + registerTaskListener(eventName, handler) { + this.$root.$once(eventName, handler); + this.taskListeners.push([eventName, handler]); }, - hideRequestCertificateModal() { - this.isShownRequestCertificateModal = false; - }, - showManageNamesModal(certificate) { - this.currentCertificate = certificate; - this.isShownRequestCertificateModal = true; - }, - showDeleteCertificateModal(certificate) { - this.currentCertificate = certificate; - this.isShownDeleteCertificateModal = true; - }, - hideDeleteCertificateModal() { - this.isShownDeleteCertificateModal = false; - }, - showDeleteObsoleteCertificatesModal() { - this.isShownDeleteObsoleteCertificatesModal = true; + clearTaskListeners() { + for (const [eventName, handler] of this.taskListeners) { + this.$root.$off(eventName, handler); + } + this.taskListeners = []; }, - hideDeleteObsoleteCertificatesModal() { - this.isShownDeleteObsoleteCertificatesModal = false; + normalizeView() { + if (!TAB_VIEWS.includes(this.q.view)) { + this.q.view = TAB_VIEWS[0]; + } }, async listInstalledModules() { + // a handler of a previous round would decrement the counters of this one + this.clearTaskListeners(); this.loading.listInstalledModules = true; + this.error.listInstalledModules = ""; const taskAction = "list-installed-modules"; const eventId = this.getUuid(); // register to task error - this.$root.$once( + this.registerTaskListener( `${taskAction}-aborted-${eventId}`, this.listInstalledModulesAborted ); // register to task completion - this.$root.$once( + this.registerTaskListener( `${taskAction}-completed-${eventId}`, this.listInstalledModulesCompleted ); @@ -855,16 +217,13 @@ export default { console.error(`error creating task ${taskAction}`, err); const errMessage = this.getErrorMessage(err); this.error.listInstalledModules = errMessage; - this.currentErrorAction = this.$t("action." + taskAction); - this.currentErrorDescription = errMessage; + this.loading.listInstalledModules = false; return; } }, listInstalledModulesAborted(taskResult, taskContext) { console.error(`${taskContext.action} aborted`, taskResult); this.error.listInstalledModules = this.$t("error.generic_error"); - this.currentErrorAction = this.$t("action." + taskContext.action); - this.currentErrorDescription = this.$t("error.generic_error"); this.loading.listInstalledModules = false; }, listInstalledModulesCompleted(taskContext, taskResult) { @@ -899,7 +258,6 @@ export default { } this.internalNodes = nodes; this.traefikInstances = traefikInstances; - this.uploadTlsCertificateState.setNodes(nodes); this.loading.listInstalledModules = false; this.$nextTick(() => { @@ -916,463 +274,14 @@ export default { }); } }); - - this.listCertificates(); - }, - async listCertificates() { - this.offlineTraefikInstances = []; - this.listCertificatesErrors = []; - this.loading.listCertificatesNum = 0; - - for (const traefikInstance of this.traefikInstances) { - const taskAction = "list-certificates"; - const eventId = this.getUuid(); - this.loading.listCertificatesNum++; - - // register to task events - - this.$root.$once( - `${taskAction}-aborted-${eventId}`, - (taskResult, taskContext) => { - this.listCertificatesAborted( - taskResult, - taskContext, - traefikInstance - ); - } - ); - - this.$root.$once( - `${taskAction}-completed-${eventId}`, - this.listCertificatesCompleted - ); - - const res = await to( - this.createModuleTaskForApp(traefikInstance.id, { - action: taskAction, - data: { - expand_list: true, - }, - extra: { - title: this.$t("action." + taskAction), - isNotificationHidden: true, - traefikInstance: traefikInstance, - eventId, - }, - }) - ); - const err = res[0]; - - if (err) { - console.error( - `error creating task ${taskAction} for instance ${traefikInstance.id} on node ${traefikInstance.node}`, - err - ); - // Clean up orphaned event listeners since the task was never created - this.$root.$off(`${taskAction}-aborted-${eventId}`); - this.$root.$off(`${taskAction}-completed-${eventId}`); - - if (err.response && err.response.status === 404) { - // 404 means the module API is unreachable: node is offline - if ( - traefikInstance && - !this.offlineTraefikInstances.find( - (instance) => instance.id === traefikInstance.id - ) - ) { - this.offlineTraefikInstances.push(traefikInstance); - } - } else { - // Unexpected error: display a generic inline error notification - this.listCertificatesErrors.push({ - title: this.$t("action." + taskAction), - description: `${this.$t( - "error.generic_error" - )} (${this.getTraefikInstanceLabel( - traefikInstance - )} - ${this.getNodeLabel(traefikInstance)})`, - }); - } - this.loading.listCertificatesNum--; - } - } - }, - listCertificatesAborted(taskResult, taskContext, traefikInstance) { - console.error( - `${taskContext.action} aborted for instance ${traefikInstance.id} on node ${traefikInstance.node}`, - taskResult - ); - - // Add error to array - this.listCertificatesErrors.push({ - title: this.$t("action." + taskContext.action), - description: `${this.$t( - "error.generic_error" - )} (${this.getTraefikInstanceLabel( - traefikInstance - )} - ${this.getNodeLabel(traefikInstance)})`, - }); - this.loading.listCertificatesNum--; - }, - listCertificatesCompleted(taskContext, taskResult) { - const traefikId = taskContext.extra.traefikInstance.id; - const nodeId = taskContext.extra.traefikInstance.node; - const nodeUiName = taskContext.extra.traefikInstance.node_ui_name; - const node = { id: nodeId, ui_name: nodeUiName }; - const nodeLabel = this.getShortNodeLabel(node) + ` (${traefikId})`; - const certs = []; - - for (let certificate of taskResult.output.certificates) { - // "Certificate" column - - if (certificate.default && certificate.type === "internal") { - certificate.ui_name = this.$t( - "settings_tls_certificates.node_name_certificate", - { node: this.getShortNodeLabel(node) } - ); - } else { - certificate.ui_name = certificate.subject; - } - - // "Type" column - - let ui_type = ""; - - if (certificate.type === "custom") { - ui_type = "uploaded"; - } else if (certificate.automatic) { - ui_type = "automatic"; - } else if (certificate.default && certificate.type === "internal") { - ui_type = "requested"; - } else if (!certificate.default && !certificate.automatic) { - ui_type = "obsolete"; - } - certificate.ui_type = ui_type; - - // "Issuer" column - - const matched = certificate.issuer.match(/,O=(.+?)(?:,|$)/); - - if (matched && matched.length > 1) { - certificate.ui_issuer = matched[1]; - } else { - certificate.ui_issuer = certificate.issuer; - } - - // "Status" column - - certificate.status = certificate.validity; - - certificate.node = nodeLabel; - certificate.nodeId = nodeId; - certificate.longNodeLabel = this.getNodeLabel(node); - certificate.traefikInstance = traefikId; - - // "Expiration" column - - const validTo = new Date(certificate.valid_to); - const formattedValidTo = validTo.toLocaleDateString( - navigator.language, - { - year: "numeric", - month: "long", - day: "numeric", - } - ); - certificate.expiration = formattedValidTo; - - certs.push(certificate); - } - - // $set() is needed for reactivity (see https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects) - this.$set(this.certificatesByTraefikId, traefikId, certs); - - this.loading.listCertificatesNum--; - }, - getTagKind(status) { - switch (status) { - case "valid": - case "expiring": - return "green"; - case "expired": - return "red"; - default: - return "gray"; - } - }, - getNodeLabel(instance) { - const nodeLabel = instance.node_ui_name - ? instance.node_ui_name + - " (" + - this.$t("common.node") + - " " + - instance.node + - ")" - : this.$t("common.node") + " " + instance.node; - return nodeLabel; - }, - getTraefikInstanceLabel(instance) { - let label = instance.id; - if (instance.ui_name && instance.ui_name.trim()) { - label = `${instance.ui_name}(${instance.id})`; - } - return label; - }, - getOfflineInstanceDescription(instance) { - return this.$t("settings_tls_certificates.certificates_not_displayed", { - instanceId: this.getTraefikInstanceLabel(instance), - }); - }, - clearFilters() { - this.q.selectedNodeId = "any"; - this.filter.text = ""; - this.filter.certificateType = "any"; - this.filter.certificateStatus = "any"; - }, - onReloadCertificates() { - this.clearFilters(); - this.listCertificates(); - }, - async deleteCertificate() { - this.loading.deleteCertificate = true; - this.error.deleteCertificate = ""; - const taskAction = "delete-certificate"; - const eventId = this.getUuid(); - - // register to task error - this.$root.$once( - `${taskAction}-aborted-${eventId}`, - this.deleteCertificateAborted - ); - - // register to task completion - this.$root.$once( - `${taskAction}-completed-${eventId}`, - this.deleteCertificateCompleted - ); - - const payload = { - type: this.currentCertificate.type, - }; - - if (this.currentCertificate.type === "custom") { - payload.path = this.currentCertificate.path; - } else { - payload.serial = this.currentCertificate.serial; - } - - const res = await to( - this.createModuleTaskForApp(this.currentCertificate.traefikInstance, { - action: taskAction, - data: payload, - extra: { - title: this.$t("settings_tls_certificates.delete_certificate"), - description: this.$t("common.processing"), - eventId, - }, - }) - ); - const err = res[0]; - - if (err) { - console.error(`error creating task ${taskAction}`, err); - this.error.deleteCertificate = this.getErrorMessage(err); - this.loading.deleteCertificate = false; - return; - } - this.hideDeleteCertificateModal(); - }, - deleteCertificateAborted(taskResult, taskContext) { - console.error(`${taskContext.action} aborted`, taskResult); - this.loading.deleteCertificate = false; - }, - deleteCertificateCompleted() { - this.loading.deleteCertificate = false; - - // reload certificates - this.listCertificates(); - }, - goToAcmeServers() { - this.$router.push("/settings/acme-servers"); - }, - async uploadCustomCertificate(event) { - // set component to loading - this.uploadTlsCertificateState.setLoading(true); - // clear error state - this.uploadTlsCertificateState.errors.keyFile = null; - this.uploadTlsCertificateState.errors.certFile = null; - this.uploadTlsCertificateState.errors.chainFile = null; - - let keyFileDecoded = await new Promise((result) => { - let keyFileReader = new FileReader(); - keyFileReader.readAsBinaryString(event.keyFile); - keyFileReader.onerror = () => { - throw Error(); - }; - keyFileReader.onload = () => { - result(window.btoa(keyFileReader.result)); - }; - }).catch(() => { - this.uploadTlsCertificateState.setKeyFileError( - this.$t("settings_tls_certificates.cannot_load_key_file") - ); - this.uploadTlsCertificateState.setLoading(false); - }); - - let certFileText = await new Promise((result) => { - let certFileReader = new FileReader(); - certFileReader.readAsBinaryString(event.certFile); - certFileReader.onerror = () => { - throw Error(); - }; - certFileReader.onload = () => { - result(certFileReader.result); - }; - }).catch(() => { - this.uploadTlsCertificateState.setCertFileErrors( - this.$t("settings_tls_certificates.cannot_load_cert_file") - ); - this.uploadTlsCertificateState.setLoading(false); - }); - - if (event.chainFile) { - let chainFileText = await new Promise((result) => { - let chainFileReader = new FileReader(); - chainFileReader.readAsBinaryString(event.chainFile); - chainFileReader.onerror = () => { - throw Error(); - }; - chainFileReader.onload = () => { - result(chainFileReader.result); - }; - }).catch(() => { - this.uploadTlsCertificateState.setCertFileErrors( - this.$t("settings_tls_certificates.cannot_load_chain_file") - ); - this.uploadTlsCertificateState.setLoading(false); - }); - certFileText += `\n${chainFileText}`; - } - const certFileDecoded = window.btoa(certFileText); - const taskAction = "upload-certificate"; - const eventId = this.getUuid(); - - // abort handler - this.$root.$once(`${taskAction}-aborted-${eventId}`, (error) => { - this.uploadTlsCertificateState.setLoading(false); - switch (error.exit_code) { - case 2: - case 3: - this.uploadTlsCertificateState.setKeyFileError(error.output); - break; - case 4: - this.uploadTlsCertificateState.setCertFileError(error.output); - break; - default: - this.uploadTlsCertificateState.setKeyFileError(error.error); - break; - } - }); - - // validation success handler - this.$root.$once(`${taskAction}-validation-ok-${eventId}`, () => { - this.uploadTlsCertificateState.setLoading(false); - this.uploadTlsCertificateState.setVisible(false); - // clear state and node combobox - this.$refs.uploadTlsCertificateModal.clear(); - }); - - // validation failed handler - this.$root.$once( - `${taskAction}-validation-failed-${eventId}`, - (validationErrors) => { - this.uploadTlsCertificateState.setLoading(false); - - for (const validationError of validationErrors) { - const param = validationError.parameter; - const errorMessage = this.getI18nStringWithFallback( - "settings_tls_certificates." + validationError.error, - "error." + validationError.error, - null, - { value: validationError.value } - ); - - switch (param) { - case "keyFile": - this.uploadTlsCertificateState.setKeyFileError(errorMessage); - break; - case "certFile": - this.uploadTlsCertificateState.setCertFileError(errorMessage); - break; - case "chainFile": - this.uploadTlsCertificateState.setChainFileError(errorMessage); - break; - default: - break; - } - } - } - ); - - // completed task - this.$root.$once(`${taskAction}-completed-${eventId}`, () => { - // clear state and node combobox - this.$refs.uploadTlsCertificateModal.clear(); - this.onReloadCertificates(); - }); - - const res = await to( - this.createModuleTaskForApp(event.targetInstance, { - action: taskAction, - data: { - keyFile: keyFileDecoded, - certFile: certFileDecoded, - }, - extra: { - title: this.$t("settings_tls_certificates.add_custom_certificate"), - description: this.$t("common.processing"), - eventId, - }, - }) - ); - const err = res[0]; - - if (err) { - console.error(`error creating task ${taskAction}`, err); - this.uploadTlsCertificateState.setKeyFileError( - this.$t("error.generic_error") - ); - this.uploadTlsCertificateState.setLoading(false); - } }, - onSearchInput() { - // workaround to detect click on clear search button - if (!this.searchFilter) { - this.$refs.certificatesTable.goToFirstPage(); - this.focusElement("tableSearch"); + tabSelected(tabNum) { + if (tabNum == 0) { + this.q.view = "certificates"; + } else if (tabNum == 1) { + this.q.view = "acme"; } }, - sortCertificates(a, b) { - if (a.ui_type !== b.ui_type) { - return b.ui_type.localeCompare(a.ui_type); - } - - // If ui_type is the same, sort by expiration - return a.valid_to.localeCompare(b.valid_to); - }, }, }; - -