diff --git a/core/ui/package.json b/core/ui/package.json index 71f901a85..215791a80 100644 --- a/core/ui/package.json +++ b/core/ui/package.json @@ -16,7 +16,7 @@ "@carbon/icons-vue": "^10.37.0", "@carbon/themes": "^10.34.0", "@carbon/vue": "^2.40.0", - "@nethserver/ns8-ui-lib": "^1.12.4", + "@nethserver/ns8-ui-lib": "^2.0.1", "await-to-js": "^3.0.0", "axios": "^0.33.0", "carbon-components": "^10.41.0", diff --git a/core/ui/public/i18n/en/translation.json b/core/ui/public/i18n/en/translation.json index 8330ac4fc..85bdeabcd 100644 --- a/core/ui/public/i18n/en/translation.json +++ b/core/ui/public/i18n/en/translation.json @@ -402,6 +402,8 @@ "set-route": "Set route", "delete-route": "Delete route", "list-routes": "List routes", + "get-trusted-proxies": "Retrieve frontend proxies", + "set-trusted-proxies": "Configure frontend proxies", "set-certificate": "Set certificate", "set-default-certificate": "Request certificate", "delete-certificate": "Delete certificate", @@ -785,16 +787,13 @@ "name": "Name", "type": "Type", "node": "Node", - "search_route": "Search route", + "search_route": "Filter routes", "no_http_route": "No HTTP route", "no_http_route_description": "There is no route configured", "create_route": "Create route", "host": "Host", "path": "Path", "host_and_path": "Host and path", - "routes_filtered": "Routes filtered", - "routes_filtered_description": "Showing only routes for {node}", - "show_all": "Show all", "http_route_details": "HTTP route details", "url": "URL", "strip_prefix": "Strip URL path prefix", @@ -844,7 +843,29 @@ "show_logs": "Show logs", "traefik_will_be_restarted": "Traefik will be restarted", "delete_route_with_certificate_message": "Deleting this route will briefly disconnect HTTP clients connected to {node}.", - "revoke_certificate_message": "Revoking the route certificate will briefly disconnect HTTP clients connected to {node}." + "revoke_certificate_message": "Revoking the route certificate will briefly disconnect HTTP clients connected to {node}.", + "routes": "Routes", + "frontend_proxies": "Frontend proxies", + "proxies": "Frontend proxies", + "depth": "Trust depth", + "add_frontend_proxy": "Add frontend proxy", + "add_frontend_proxy_disabled_message": "All cluster nodes already have a frontend proxy", + "add_frontend_proxy_no_node_message": "No cluster node with a Traefik instance is available", + "edit_frontend_proxy_node": "Edit frontend proxies of {node}", + "delete_frontend_proxy_node": "Delete frontend proxies of {node}", + "delete_frontend_proxy_description": "Delete the frontend proxies of {node}? Traefik will no longer see the real client IP address.", + "frontend_proxies_description": "A cluster node can stand behind a reverse proxy or a load balancer. Traefik trusts the X-Forwarded-For header of the addresses listed here to recover the original client IP.", + "no_frontend_proxy": "No frontend proxy configured", + "no_frontend_proxy_description": "Add the addresses of the proxies in front of a cluster node.", + "filter_frontend_proxies": "Filter", + "choose_node": "Choose node", + "frontend_proxies_helper": "Enter one IPv4 or IPv6 address per line", + "trust_depth": "Trust depth", + "trust_depth_helper": "Number of frontend proxy levels", + "trust_depth_tooltip": "With a trust depth of 1, the client IP address is read from the last entry of the X-Forwarded-For header.", + "trust_depth_invalid": "Enter a number of 1 or higher", + "bad_ip_address": "Invalid IP address. Enter one IPv4 or IPv6 address per line, without CIDR prefix", + "frontend_proxy_restart_message": "HTTP clients on {node} will be briefly disconnected. The connection should resume automatically — otherwise, reload the page manually." }, "settings_tls_certificates": { "title": "TLS certificates", diff --git a/core/ui/src/components/settings/ConfigureFrontendProxyModal.vue b/core/ui/src/components/settings/ConfigureFrontendProxyModal.vue new file mode 100644 index 000000000..1af8f3b18 --- /dev/null +++ b/core/ui/src/components/settings/ConfigureFrontendProxyModal.vue @@ -0,0 +1,421 @@ + + + + {{ + isEditing + ? $t("settings_http_routes.edit_frontend_proxy_node", { + node: proxyConfig ? proxyConfig.nodeLabel : "", + }) + : $t("settings_http_routes.add_frontend_proxy") + }} + + + + + + + + + {{ $t("settings_http_routes.trust_depth") }} + + + {{ $t("settings_http_routes.trust_depth_tooltip") }} + + + + + + + + + + + + {{ $t("common.cancel") }} + {{ + isEditing + ? $t("common.save") + : $t("settings_http_routes.add_frontend_proxy") + }} + + + + + + diff --git a/core/ui/src/components/settings/CreateOrEditHttpRouteModal.vue b/core/ui/src/components/settings/CreateOrEditHttpRouteModal.vue index 30080c89a..92e64b9d7 100644 --- a/core/ui/src/components/settings/CreateOrEditHttpRouteModal.vue +++ b/core/ui/src/components/settings/CreateOrEditHttpRouteModal.vue @@ -266,10 +266,11 @@ + + diff --git a/core/ui/src/components/settings/HttpRoutesPanel.vue b/core/ui/src/components/settings/HttpRoutesPanel.vue new file mode 100644 index 000000000..65364b8ee --- /dev/null +++ b/core/ui/src/components/settings/HttpRoutesPanel.vue @@ -0,0 +1,800 @@ + + + + + + + + + {{ $t("settings_http_routes.create_route") }} + + + + + + + + + + + {{ $t("common.clear_filters") }} + + + + + + + + + + + + + + + + + {{ $t("common.no_search_results_description") }} + + {{ $t("common.clear_filters") }} + + + + + + + + + + + + + + {{ + $t("settings_http_routes.no_http_route_description") + }} + + + + + + + + + + + {{ row.host }}{{ row.path }} + + + {{ row.host || row.path }} + + + + + + + {{ + $t( + "settings_http_routes.cannot_obtain_tls_certificate" + ) + }} + + {{ $t("settings_http_routes.show_logs") }} + + + + + + + + {{ row.name }} + + + + + {{ $t(`settings_http_routes.${row.type}`) }} + + + + {{ row.node }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/mixins/ipAddress.js b/core/ui/src/mixins/ipAddress.js new file mode 100644 index 000000000..47d50d842 --- /dev/null +++ b/core/ui/src/mixins/ipAddress.js @@ -0,0 +1,33 @@ +const IPV4_OCTET = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"; +const IPV4 = `${IPV4_OCTET}(\\.${IPV4_OCTET}){3}`; +const IPV6 = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}${IPV4}|([0-9a-fA-F]{1,4}:){1,4}:${IPV4})`; +const IPV4_PREFIX = "(\\/([0-9]|[1-2][0-9]|3[0-2]))?"; +const IPV6_PREFIX = "(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?"; + +const IPV4_PATTERN = new RegExp(`^${IPV4}$`); +const IPV6_PATTERN = new RegExp(`^${IPV6}$`); +const IPV4_CIDR_PATTERN = new RegExp(`^${IPV4}${IPV4_PREFIX}$`); +const IPV6_CIDR_PATTERN = new RegExp(`^${IPV6}${IPV6_PREFIX}$`); + +export default { + name: "IpAddressService", + methods: { + isIpv4Address(value) { + return IPV4_PATTERN.test(value); + }, + isIpv6Address(value) { + return IPV6_PATTERN.test(value); + }, + // no prefix: set-trusted-proxies validates with ipaddress.ip_address() + isIpAddress(value) { + return this.isIpv4Address(value) || this.isIpv6Address(value); + }, + isIpAddressOrCidr(value) { + return IPV4_CIDR_PATTERN.test(value) || IPV6_CIDR_PATTERN.test(value); + }, + // picks the error message: a rejected value with a colon was meant as IPv6 + looksLikeIpv6(value) { + return value.includes(":"); + }, + }, +}; diff --git a/core/ui/src/views/settings/SettingsHttpRoutes.vue b/core/ui/src/views/settings/SettingsHttpRoutes.vue index 28c3e1898..972b80f62 100644 --- a/core/ui/src/views/settings/SettingsHttpRoutes.vue +++ b/core/ui/src/views/settings/SettingsHttpRoutes.vue @@ -38,279 +38,42 @@ - - - - - - - - - - - - - - - - {{ $t("settings_http_routes.create_route") }} - - - - - - - - - - - {{ - $t( - "settings_http_routes.no_http_route_description" - ) - }} - - - - - - - - - - {{ row.host }}{{ row.path }} - - - {{ row.host || row.path }} - - - - - - - {{ - $t( - "settings_http_routes.cannot_obtain_tls_certificate" - ) - }} - - {{ $t("settings_http_routes.show_logs") }} - - - - - - - - {{ row.name }} - - - - - {{ $t(`settings_http_routes.${row.type}`) }} - - - - {{ row.node }} - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - @@ -320,114 +83,75 @@ import { QueryParamService, UtilService, TaskService, - IconService, PageTitleService, - DateTimeService, } from "@nethserver/ns8-ui-lib"; import { mapState } from "vuex"; -import HttpRouteDetailModal from "@/components/settings/HttpRouteDetailModal.vue"; -import CreateOrEditHttpRouteModal from "@/components/settings/CreateOrEditHttpRouteModal.vue"; -import _cloneDeep from "lodash/cloneDeep"; -import WarningAltFilled16 from "@carbon/icons-vue/es/warning--alt--filled/16"; +import HttpRoutesPanel from "@/components/settings/HttpRoutesPanel.vue"; +import HttpFrontendProxiesPanel from "@/components/settings/HttpFrontendProxiesPanel.vue"; + +// tab order: the view query param is the tab selector +const VIEWS = ["routes", "frontend-proxies"]; export default { name: "SettingsHttpRoutes", components: { - HttpRouteDetailModal, - CreateOrEditHttpRouteModal, - WarningAltFilled16, + HttpRoutesPanel, + HttpFrontendProxiesPanel, }, - mixins: [ - TaskService, - UtilService, - IconService, - QueryParamService, - PageTitleService, - DateTimeService, - ], + mixins: [TaskService, UtilService, QueryParamService, PageTitleService], pageTitle() { return this.$t("settings_http_routes.title"); }, data() { return { q: { + view: "", selectedNodeId: "", + // one param per tab: the two tables filter independently + proxyNodeId: "", }, - tablePage: [], - tableColumns: ["route", "name", "type", "node", "attributes"], - routes: [], - internalNodes: [], - isShownCreateOrEditRouteModal: false, - isShownRouteDetailModal: false, - isShownDeleteRouteModal: false, - currentErrorAction: "", - currentErrorDescription: "", traefikInstances: [], - currentRoute: null, - routeToDelete: null, - isEditingRoute: false, - pendingCertificatesLogsPath: {}, + // [eventName, handler] pairs registered on $root for the read chain + readListeners: [], loading: { listInstalledModules: false, - listRoutesNum: 0, - deleteRoute: false, }, error: { listInstalledModules: "", - listRoutes: "", - deleteRoute: "", }, }; }, computed: { - ...mapState(["clusterNodes"]), - i18nTableColumns() { - return this.tableColumns.map((column) => { - return this.$t("settings_http_routes." + column); - }); - }, - loadingRoutes() { - return ( - this.loading.listInstalledModules || this.loading.listRoutesNum > 0 - ); - }, - filteredRoutes() { - if (this.q.selectedNodeId === "all" || !this.q.selectedNodeId) { - return this.routes; - } + ...mapState(["clusterNodes", "isWebsocketConnected"]), + // clusterNodes can land after list-installed-modules, on a page reload it + // usually does: recompute the list instead of snapshotting it once + internalNodes() { + return this.clusterNodes.map((node) => { + const nodeId = node.id.toString(); + const traefikInstance = this.traefikInstances.find( + (instance) => instance.node === nodeId + ); + const internalNode = { + name: nodeId, + label: this.getShortNodeLabel(node), + value: nodeId, + }; - return this.routes.filter((route) => { - return route.nodeId === this.q.selectedNodeId; + if (traefikInstance) { + internalNode.label += ` (${traefikInstance.id})`; + internalNode.traefikInstance = traefikInstance.id; + } + return internalNode; }); }, - selectedNodeLabel() { - if (this.q.selectedNodeId === "all" || !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 []; + }, + watch: { + isWebsocketConnected: function (isConnected) { + // a Traefik restart kills this websocket: pending task events are lost. + // Always restart from the instance list, it chains both tabs + if (isConnected) { + this.listInstalledModules(); } - - // add "All nodes" at the beginning of internalNodes array - const nodes = _cloneDeep(this.internalNodes); - - nodes.unshift({ - name: "all", - label: this.$t("common.any_node"), - value: "all", - }); - return nodes; }, }, beforeRouteEnter(to, from, next) { @@ -441,48 +165,53 @@ export default { next(); }, created() { - this.listInstalledModules(); + // the watcher fires the first read when the websocket is not up yet + if (this.isWebsocketConnected) { + this.listInstalledModules(); + } + }, + beforeDestroy() { + // a task completing after destroy would re-fire the whole read chain + this.clearListeners(this.readListeners); + }, + mounted() { + // the tabs have no default selection: an unknown view would leave them all closed + if (!VIEWS.includes(this.q.view)) { + this.q.view = "routes"; + } }, methods: { - showCreateRouteModal() { - this.isEditingRoute = false; - this.isShownCreateOrEditRouteModal = true; - }, - showEditRouteModal(route) { - this.isEditingRoute = true; - this.currentRoute = route; - this.isShownCreateOrEditRouteModal = true; - }, - hideCreateRouteModal() { - this.isShownCreateOrEditRouteModal = false; - }, - showRouteDetailModal(route) { - this.currentRoute = route; - this.isShownRouteDetailModal = true; - }, - hideRouteDetailModal() { - this.isShownRouteDetailModal = false; + tabSelected(tabNum) { + this.q.view = VIEWS[tabNum]; }, - showDeleteRouteModal(route) { - this.routeToDelete = route; - this.isShownDeleteRouteModal = true; + registerListener(listeners, eventName, handler) { + this.$root.$once(eventName, handler); + listeners.push([eventName, handler]); }, - hideDeleteRouteModal() { - this.isShownDeleteRouteModal = false; + clearListeners(listeners) { + listeners.forEach(([eventName, handler]) => { + this.$root.$off(eventName, handler); + }); + listeners.splice(0); }, async listInstalledModules() { + // a handler of a previous chain would decrement the counter of this one + this.clearListeners(this.readListeners); this.loading.listInstalledModules = true; + this.error.listInstalledModules = ""; const taskAction = "list-installed-modules"; const eventId = this.getUuid(); // register to task error - this.$root.$once( + this.registerListener( + this.readListeners, `${taskAction}-aborted-${eventId}`, this.listInstalledModulesAborted ); // register to task completion - this.$root.$once( + this.registerListener( + this.readListeners, `${taskAction}-completed-${eventId}`, this.listInstalledModulesCompleted ); @@ -499,301 +228,30 @@ export default { const err = res[0]; if (err) { 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; - return; + this.error.listInstalledModules = this.getErrorMessage(err); + // otherwise the tables keep their skeleton rows for good + this.loading.listInstalledModules = false; } }, 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) { - // init nodes - let nodes = []; - - for (let node of this.clusterNodes) { - nodes.push({ - name: node.id.toString(), - label: this.getShortNodeLabel(node), - value: node.id.toString(), - }); - } - let traefikInstances = []; for (let instanceList of Object.values(taskResult.output)) { for (let instance of instanceList) { if (instance.id.startsWith("traefik")) { traefikInstances.push(instance); - - // update nodes labels - const node = nodes.find((node) => node.value === instance.node); - - if (node) { - node.label += ` (${instance.id})`; - node.traefikInstance = instance.id; - } } } } - this.internalNodes = nodes; + // a new array, not an in-place mutation: both panels reload from a + // traefikInstances watcher this.traefikInstances = traefikInstances; this.loading.listInstalledModules = false; - - this.$nextTick(() => { - if (!this.q.selectedNodeId) { - // initially show all nodes - this.q.selectedNodeId = "all"; - } else { - const nodeId = this.q.selectedNodeId; - - // workaround to update combo box - this.q.selectedNodeId = ""; - this.$nextTick(() => { - this.q.selectedNodeId = nodeId; - }); - } - }); - - this.listRoutes(); - }, - async listRoutes() { - this.routes = []; - - for (const traefikInstance of this.traefikInstances) { - const taskAction = "list-routes"; - const eventId = this.getUuid(); - this.loading.listRoutesNum++; - - // register to task events - - this.$root.$once( - `${taskAction}-aborted-${eventId}`, - this.listRoutesAborted - ); - - this.$root.$once( - `${taskAction}-completed-${eventId}`, - this.listRoutesCompleted - ); - - 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}`, err); - const errMessage = this.getErrorMessage(err); - this.error.listRoutes = errMessage; - this.currentErrorAction = this.$t("action." + taskAction); - this.currentErrorDescription = errMessage; - this.loading.listRoutesNum--; - } - } - }, - listRoutesAborted(taskResult, taskContext) { - console.error(`${taskContext.action} aborted`, taskResult); - this.error.listRoutes = this.$t("error.generic_error"); - this.currentErrorAction = this.$t("action." + taskContext.action); - this.currentErrorDescription = this.$t("error.generic_error"); - this.loading.listRoutesNum--; - }, - listRoutesCompleted(taskContext, taskResult) { - const routes = []; - const traefikId = taskContext.extra.traefikInstance.id; - const nodeId = taskContext.extra.traefikInstance.node; - const nodeUiName = taskContext.extra.traefikInstance.node_ui_name; - - for (let route of taskResult.output) { - route.name = route.instance; - - let type = ""; - if (route.host && route.path) { - type = "host_and_path"; - } else if (route.host) { - type = "host"; - } else if (route.path) { - type = "path"; - } - route.type = type; - route.ip_allowlist = route.ip_allowlist ? route.ip_allowlist : []; - route.ip_allowlist_str = route.ip_allowlist - ? route.ip_allowlist.join("\n") - : ""; - - const node = { id: nodeId, ui_name: nodeUiName }; - const nodeLabel = this.getShortNodeLabel(node) + ` (${traefikId})`; - route.node = nodeLabel; - route.nodeId = nodeId; - route.longNodeLabel = this.getNodeLabel(node); - route.traefikInstance = traefikId; - routes.push(route); - } - this.routes = this.routes - .concat(routes) - .sort(this.sortByProperty("name")); - this.loading.listRoutesNum--; - - // compute logs path for pending certificates - - const now = new Date(); - const threeDaysAgo = new Date(now.getTime() - 72 * 60 * 60 * 1000); - const startDate = this.formatDate(threeDaysAgo, "yyyy-MM-dd"); - const startHours = this.formatDate(threeDaysAgo, "HH"); - const startMins = this.formatDate(threeDaysAgo, "mm"); - const startTime = `${startHours}%3A${startMins}`; - const endDate = this.formatDate(now, "yyyy-MM-dd"); - const endHours = this.formatDate(now, "HH"); - const endMins = this.formatDate(now, "mm"); - const endTime = `${endHours}%3A${endMins}`; - const maxLines = 10; - - this.pendingCertificatesLogsPath[ - traefikId - ] = `?searchQuery=acmeCA%3D&context=module&selectedAppId=${traefikId}&followLogs=false&startDate=${startDate}&startTime=${startTime}&endDate=${endDate}&endTime=${endTime}&maxLines=${maxLines}&autoStartSearch=true`; - }, - showAllNodes() { - this.q.selectedNodeId = "all"; - }, - onReloadRoutes() { - this.listRoutes(); - }, - async deleteRoute() { - this.loading.deleteRoute = true; - this.error.deleteRoute = ""; - const taskAction = "delete-route"; - const eventId = this.getUuid(); - - // register to task error - this.$root.$once( - `${taskAction}-aborted-${eventId}`, - this.deleteRouteAborted - ); - - // register to task completion - this.$root.$once( - `${taskAction}-completed-${eventId}`, - this.deleteRouteCompleted - ); - - const res = await to( - this.createModuleTaskForApp(this.routeToDelete.traefikInstance, { - action: taskAction, - data: { - instance: this.routeToDelete.name, - lets_encrypt_cleanup: true, - }, - extra: { - title: this.$t("settings_http_routes.delete_route_route", { - route: this.routeToDelete.name, - }), - description: this.$t("common.processing"), - eventId, - }, - }) - ); - const err = res[0]; - - if (err) { - console.error(`error creating task ${taskAction}`, err); - this.error.deleteRoute = this.getErrorMessage(err); - this.loading.deleteRoute = false; - return; - } - this.hideDeleteRouteModal(); - }, - deleteRouteAborted(taskResult, taskContext) { - console.error(`${taskContext.action} aborted`, taskResult); - this.loading.deleteRoute = false; - }, - deleteRouteCompleted() { - this.loading.deleteRoute = false; - - // reload routes - this.listRoutes(); - }, - filterHttpRoutes(searchFilter) { - if (!searchFilter) { - return this.filteredRoutes; - } else { - // clean query - const cleanRegex = /[^a-zA-Z0-9]/g; - const queryText = searchFilter.replace(cleanRegex, ""); - const searchFields = ["host", "path", "name", "type", "node"]; - - const searchResults = this.filteredRoutes.filter((option) => { - // compare query text with host + path (if present) - if (option.host && option.path) { - const hostAndPath = `${option.host}${option.path}`; - - const hostAndPathMatches = new RegExp(queryText, "i").test( - hostAndPath.replace(cleanRegex, "") - ); - - if (hostAndPathMatches) { - return true; - } - } - - // compare query text with attributes - return searchFields.some((searchField) => { - const searchValue = option[searchField]; - - if (searchValue) { - return new RegExp(queryText, "i").test( - searchValue.replace(cleanRegex, "") - ); - } else { - return false; - } - }); - }, this); - return searchResults; - } - }, - sortHttpRoutes(sortProperty) { - if (sortProperty != "route") { - // default sort - return this.sortByProperty(sortProperty); - } - - // sort by route (host or path) - - return function (a, b) { - const valueA = a.host || a.path; - const valueB = b.host || b.path; - - if (valueA < valueB) { - return -1; - } - if (valueA > valueB) { - return 1; - } - return 0; - }; - }, - goToPendingCertificateLogs(route) { - this.$router.push( - `/system-logs${this.pendingCertificatesLogsPath[route.traefikInstance]}` - ); }, }, }; diff --git a/core/ui/yarn.lock b/core/ui/yarn.lock index fae5d8163..169a8b507 100644 --- a/core/ui/yarn.lock +++ b/core/ui/yarn.lock @@ -3289,9 +3289,9 @@ __metadata: languageName: node linkType: hard -"@nethserver/ns8-ui-lib@npm:^1.12.4": - version: 1.12.4 - resolution: "@nethserver/ns8-ui-lib@npm:1.12.4" +"@nethserver/ns8-ui-lib@npm:^2.0.1": + version: 2.0.1 + resolution: "@nethserver/ns8-ui-lib@npm:2.0.1" dependencies: "@rollup/plugin-json": ^4.1.0 core-js: ^3.15.2 @@ -3300,7 +3300,7 @@ __metadata: vue-date-fns: ^2.0.1 peerDependencies: vue: ^2.6.12 - checksum: 319c09ec851a7391c49da77f1849d09a40da199349e5d797d1dd6d09b688a316f6bcc4cacd0018dc7a2348ccda9f155bf25ceb981720e854dde85490f8c28257 + checksum: 1f87dc7ae9994d8c2ebe6348714f900f39096975be4f24af10b2adfa1c84e53b8782ae379d3c71b19873361d46a1f426cb8db23f7d69ae749059f3db138520a6 languageName: node linkType: hard @@ -14771,7 +14771,7 @@ __metadata: "@carbon/icons-vue": ^10.37.0 "@carbon/themes": ^10.34.0 "@carbon/vue": ^2.40.0 - "@nethserver/ns8-ui-lib": ^1.12.4 + "@nethserver/ns8-ui-lib": ^2.0.1 "@storybook/addon-actions": ^6.2.9 "@storybook/addon-essentials": ^6.2.9 "@storybook/addon-links": ^6.2.9
+ {{ $t("common.no_search_results_description") }} +