-
-
-
-
-
-
-
-
-
-
- {{ $t("common.clear_filters") }}
-
-
-
-
-
-
-
-
-
-
- {{ $t("common.no_search_results_description") }}
-
-
{{ $t("common.clear_filters") }}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
- {{
- $t(
- "settings_tls_certificates.no_tls_certificate_description"
- )
- }}
-
-
- {{
- $t(
- "settings_tls_certificates.request_certificate"
- )
- }}
-
- {{
- $t(
- "settings_tls_certificates.add_custom_certificate"
- )
- }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -526,6 +592,10 @@ import { StateManager as UploadTlsCertificateState } from "@/components/settings
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 AcmeSettings from "@/components/settings/AcmeSettings.vue";
+
+// order must match the cv-tab order: tabSelected maps the tab index
+const TAB_VIEWS = ["certificates", "acme"];
export default {
name: "SettingsTlsCertificates",
@@ -534,6 +604,7 @@ export default {
UploadTlsCertificateModal,
DeleteObsoleteCertificatesModal,
WarningAltFilled16,
+ AcmeSettings,
},
mixins: [
TaskService,
@@ -549,8 +620,11 @@ export default {
data() {
return {
q: {
+ view: "",
selectedNodeId: "",
},
+ // mount once and keep alive: a v-if would re-run the task chain
+ acmeTabVisited: false,
tablePage: [],
tableColumns: [
"ui_name",
@@ -570,6 +644,8 @@ export default {
currentErrorDescription: "",
traefikInstances: [],
currentCertificate: null,
+ // [eventName, handler] pairs registered on $root
+ taskListeners: [],
filter: {
text: "",
certificateType: "",
@@ -641,7 +717,7 @@ export default {
};
},
computed: {
- ...mapState(["clusterNodes"]),
+ ...mapState(["clusterNodes", "isWebsocketConnected"]),
i18nTableColumns() {
return this.tableColumns.map((column) => {
return this.$t("settings_tls_certificates." + column);
@@ -778,12 +854,35 @@ export default {
next((vm) => {
vm.watchQueryData(vm);
vm.queryParamsToDataForCore(vm, to.query);
+ // with noDefaultToFirst, an unknown or empty view selects no tab at all
+ vm.normalizeView();
});
},
beforeRouteUpdate(to, from, next) {
this.queryParamsToDataForCore(this, to.query);
+ // queryParamsToDataForCore skips keys absent from the query
+ if (typeof to.query.view === "undefined") {
+ this.q.view = TAB_VIEWS[0];
+ }
+ this.normalizeView();
next();
},
+ watch: {
+ isWebsocketConnected: function (isConnected) {
+ // a Traefik restart kills this websocket: pending task events are lost.
+ // Always restart from the instance list, it chains everything else and
+ // republishes traefikInstances for the ACME tab
+ if (isConnected) {
+ this.listInstalledModules();
+ }
+ },
+ "q.view": function (view) {
+ // covers both the tab click and the deep link
+ if (view === "acme") {
+ this.acmeTabVisited = true;
+ }
+ },
+ },
created() {
// register to events
this.$root.$on("reloadCertificates", this.onReloadCertificates);
@@ -798,8 +897,19 @@ export default {
beforeDestroy() {
// remove event listeners
this.$root.$off("reloadCertificates");
+ this.clearTaskListeners();
},
methods: {
+ registerTaskListener(eventName, handler) {
+ this.$root.$once(eventName, handler);
+ this.taskListeners.push([eventName, handler]);
+ },
+ clearTaskListeners() {
+ this.taskListeners.forEach(([eventName, handler]) => {
+ this.$root.$off(eventName, handler);
+ });
+ this.taskListeners = [];
+ },
showRequestCertificateModal() {
this.currentCertificate = null;
this.isShownRequestCertificateModal = true;
@@ -825,18 +935,21 @@ export default {
this.isShownDeleteObsoleteCertificatesModal = false;
},
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
);
@@ -857,6 +970,7 @@ export default {
this.error.listInstalledModules = errMessage;
this.currentErrorAction = this.$t("action." + taskAction);
this.currentErrorDescription = errMessage;
+ this.loading.listInstalledModules = false;
return;
}
},
@@ -866,6 +980,8 @@ export default {
this.currentErrorAction = this.$t("action." + taskContext.action);
this.currentErrorDescription = this.$t("error.generic_error");
this.loading.listInstalledModules = false;
+ // the certificates chain will not run: release its skeleton rows
+ this.loading.listCertificatesNum = 0;
},
listInstalledModulesCompleted(taskContext, taskResult) {
// init nodes
@@ -920,18 +1036,21 @@ export default {
this.listCertificates();
},
async listCertificates() {
+ // a handler of a previous round would decrement the counter of this one
+ this.clearTaskListeners();
this.offlineTraefikInstances = [];
this.listCertificatesErrors = [];
- this.loading.listCertificatesNum = 0;
+ // count the whole batch upfront: the counter must not reach zero between
+ // two iterations
+ this.loading.listCertificatesNum = this.traefikInstances.length;
for (const traefikInstance of this.traefikInstances) {
const taskAction = "list-certificates";
const eventId = this.getUuid();
- this.loading.listCertificatesNum++;
// register to task events
- this.$root.$once(
+ this.registerTaskListener(
`${taskAction}-aborted-${eventId}`,
(taskResult, taskContext) => {
this.listCertificatesAborted(
@@ -942,7 +1061,7 @@ export default {
}
);
- this.$root.$once(
+ this.registerTaskListener(
`${taskAction}-completed-${eventId}`,
this.listCertificatesCompleted
);
@@ -1192,8 +1311,17 @@ export default {
// reload certificates
this.listCertificates();
},
- goToAcmeServers() {
- this.$router.push("/settings/acme-servers");
+ normalizeView() {
+ if (!TAB_VIEWS.includes(this.q.view)) {
+ this.q.view = TAB_VIEWS[0];
+ }
+ },
+ tabSelected(tabNum) {
+ if (tabNum == 0) {
+ this.q.view = "certificates";
+ } else if (tabNum == 1) {
+ this.q.view = "acme";
+ }
},
async uploadCustomCertificate(event) {
// set component to loading