From 911d30d7ba98ec61a714c963b75d7138442300c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 09:53:30 +0800 Subject: [PATCH] feat: SB Enhancements (NS selection and timeout) (#345) (#360) * feat: add namespace field * feat: add optional inputs * feat: refine code * feat: add feature flag * refactor: fix lint error * feat: filter default namespaces * refactor: hide tips with feature flag * refactor: use UnitInput * feat: load default value from settings * refactor: fix API url * refactor: no available namespaces * chore: update subject-case rule --------- Signed-off-by: Yi-Ya Chen (cherry picked from commit be421054d83e21375bfe22f73b30e557a109ce83) Co-authored-by: Yiya Chen --- commitlint.config.js | 2 +- .../settings/support-bundle-namespaces.vue | 77 ++-- pkg/harvester/config/doc-links.js | 11 +- pkg/harvester/config/feature-flags.js | 3 +- .../dialog/HarvesterSupportBundle.vue | 334 +++++++++++++----- pkg/harvester/l10n/en-us.yaml | 20 +- 6 files changed, 329 insertions(+), 118 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index d151338f..a05ba967 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -24,7 +24,7 @@ module.exports = { ], 'type-case': [2, 'always', 'lower-case'], 'type-empty': [2, 'never'], - 'subject-case': [2, 'always', 'lower-case'], + 'subject-case': [0, 'never'], 'subject-empty': [2, 'never'], 'subject-full-stop': [2, 'never', '.'], 'subject-max-length': [2, 'always', 72], diff --git a/pkg/harvester/components/settings/support-bundle-namespaces.vue b/pkg/harvester/components/settings/support-bundle-namespaces.vue index 55d2121f..b0aeb9b9 100644 --- a/pkg/harvester/components/settings/support-bundle-namespaces.vue +++ b/pkg/harvester/components/settings/support-bundle-namespaces.vue @@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types'; import CreateEditView from '@shell/mixins/create-edit-view'; import LabeledSelect from '@shell/components/form/LabeledSelect'; +const SELECT_ALL = 'select_all'; +const UNSELECT_ALL = 'unselect_all'; + export default { name: 'HarvesterBundleNamespaces', @@ -11,47 +14,76 @@ export default { mixins: [CreateEditView], async fetch() { + this.loading = true; + await this.$store.dispatch('harvester/findAll', { type: NAMESPACE }); + + try { + const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle'); + const response = await this.$store.dispatch('harvester/request', { url }); + + this.defaultNamespaces = response.data || []; + } catch (error) { + this.defaultNamespaces = []; + } finally { + this.loading = false; + } }, data() { - let namespaces = []; const namespacesStr = this.value?.value || this.value?.default || ''; + const namespaces = namespacesStr ? namespacesStr.split(',') : []; - if (namespacesStr) { - namespaces = namespacesStr.split(','); - } - - return { namespaces }; + return { + namespaces, + defaultNamespaces: [], + loading: true + }; }, computed: { + allNamespaces() { + return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id); + }, + + filteredNamespaces() { + const defaultIds = this.defaultNamespaces.map((ns) => ns.id); + + return this.allNamespaces.filter((ns) => !defaultIds.includes(ns)); + }, + namespaceOptions() { - return this.$store.getters['harvester/all'](NAMESPACE).map((N) => { - return { - label: N.id, - value: N.id - }; - }); + if (this.availableNamespaces.length === 0) return []; + + const allSelected = + this.namespaces.length === this.filteredNamespaces.length && + this.filteredNamespaces.every((ns) => this.namespaces.includes(ns)); + + const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL }; + + return [controlOption, ...this.filteredNamespaces]; } }, methods: { - update() { - const namespaceStr = this.namespaces.join(','); + update(selected) { + if (selected.includes(SELECT_ALL)) { + this.namespaces = [...this.filteredNamespaces]; + } else if (selected.includes(UNSELECT_ALL)) { + this.namespaces = []; + } else { + this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL); + } - this.value['value'] = namespaceStr; + this.value.value = this.namespaces.join(','); } }, watch: { - 'value.value': { - handler(neu) { - if (neu === this.value.default || !neu) { - this.namespaces = []; - } - }, - deep: true + 'value.value'(newVal) { + const raw = newVal || this.value.default || ''; + + this.namespaces = raw ? raw.split(',') : []; } } }; @@ -62,6 +94,7 @@ export default {
+import { NAMESPACE } from '@shell/config/types'; import { randomStr } from '@shell/utils/string'; import { exceptionToErrorsArray, stringify } from '@shell/utils/error'; import { LabeledInput } from '@components/Form/LabeledInput'; @@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton'; import GraphCircle from '@shell/components/graph/Circle'; import { Banner } from '@components/Banner'; import AppModal from '@shell/components/AppModal'; +import LabeledSelect from '@shell/components/form/LabeledSelect'; +import UnitInput from '@shell/components/form/UnitInput'; import { HCI } from '../types'; +import { HCI_SETTING } from '../config/settings'; +import { DOC } from '../config/doc-links'; +import { docLink } from '../utils/feature-flags'; + +const SELECT_ALL = 'select_all'; +const UNSELECT_ALL = 'unselect_all'; export default { name: 'SupportBundle', @@ -17,14 +26,36 @@ export default { AsyncButton, Banner, AppModal, + LabeledSelect, + UnitInput + }, + + async fetch() { + await this.$store.dispatch('harvester/findAll', { type: NAMESPACE }); + + try { + const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle'); + const response = await this.$store.dispatch('harvester/request', { url }); + + this.defaultNamespaces = response.data || []; + } catch (error) { + this.defaultNamespaces = []; + } }, data() { return { - url: '', - description: '', - errors: [], - isOpen: false, + isOpen: false, + errors: [], + version: '', + clusterName: '', + url: '', + description: '', + namespaces: [], + defaultNamespaces: [], + timeout: '', + expiration: '', + nodeTimeout: '', }; }, @@ -39,23 +70,51 @@ export default { percentage() { return this.$store.getters['harvester-common/getBundlePercentage']; - } + }, + + availableNamespaces() { + const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id); + const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id); + + return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns)); + }, + + namespaceOptions() { + if (this.availableNamespaces.length === 0) return []; + + const allSelected = this.namespaces.length === this.availableNamespaces.length && + this.availableNamespaces.every((ns) => this.namespaces.includes(ns)); + + const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL }; + + return [controlOption, ...this.availableNamespaces]; + }, + + docLink() { + const version = this.$store.getters['harvester-common/getServerVersion'](); + + return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version); + }, + + customSupportBundleFeatureEnabled() { + return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle'); + }, }, watch: { isShowBundleModal: { + immediate: true, handler(show) { - if (show) { - this.$nextTick(() => { - this.isOpen = true; - }); - } else { - this.isOpen = false; - this.url = ''; - this.description = ''; - } - }, - immediate: true + this.isOpen = show; + } + }, + + isOpen(newVal) { + if (newVal) { + this.loadDefaultSettings(); + } else { + this.resetForm(); + } }, }, @@ -65,33 +124,93 @@ export default { close() { this.isOpen = false; this.$store.commit('harvester-common/toggleBundleModal', false); - this.backUpName = ''; + }, + + loadDefaultSettings() { + const cluster = this.$store.getters['currentCluster']; + const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION); + const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES); + const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT); + const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION); + const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT); + + this.version = versionSetting?.currentVersion || ''; + this.clusterName = cluster?.id || ''; + this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns); + this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? ''; + this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? ''; + this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? ''; + this.url = ''; + this.description = ''; + this.errors = []; + }, + + resetForm() { + this.url = ''; + this.description = ''; + this.namespaces = []; + this.timeout = ''; + this.expiration = ''; + this.nodeTimeout = ''; + this.errors = []; + }, + + updateNamespaces(selected) { + if (selected.includes(SELECT_ALL)) { + this.namespaces = [...this.availableNamespaces]; + } else if (selected.includes(UNSELECT_ALL)) { + this.namespaces = []; + } else { + this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL); + } + }, + + updateNumberValue(field, value) { + if (value === '' || value === null || isNaN(value)) { + this[field] = ''; + + return; + } + + const num = Number(value); + const isValid = Number.isInteger(num) && num >= 0; + + this[field] = isValid ? String(num) : ''; + }, + + onKeyDown(e) { + if (['e', 'E', '+', '-', '.'].includes(e.key)) { + e.preventDefault(); + } }, async save(buttonCb) { this.errors = []; - const name = `bundle-${ randomStr(5).toLowerCase() }`; + const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`; const namespace = 'harvester-system'; + const spec = { + description: this.description.trim(), + ...(this.url.trim() && { issueURL: this.url.trim() }), + ...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }), + ...(this.timeout !== '' && { timeout: Number(this.timeout) }), + ...(this.expiration !== '' && { expiration: Number(this.expiration) }), + ...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }), + }; + const bundleCrd = { apiVersion: 'harvesterhci.io/v1beta1', type: HCI.SUPPORT_BUNDLE, kind: 'SupportBundle', - metadata: { - name, - namespace - }, - spec: { - issueURL: this.url, - description: this.description - } + metadata: { name, namespace }, + spec, }; - const inStore = this.$store.getters['currentProduct'].inStore; - const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd); - try { + const inStore = this.$store.getters['currentProduct'].inStore; + const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd); + await bundleValue.save(); this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true }); @@ -118,69 +237,110 @@ export default { @close="close" >
-

- {{ t('harvester.modal.bundle.title') }} -

- -
- - - -
- -
-
+

{{ t('harvester.modal.bundle.title') }}

+
+
-
+ + +
- {{ t('generic.close') }} - - - + +
+
@@ -194,6 +354,10 @@ export default { max-height: 100vh; } + .labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) { + margin: 0 7px; + } + .bundle { cursor: pointer; color: var(--primary); @@ -204,10 +368,8 @@ export default { } .content { - height: 218px; - .circle { - padding-top: 20px; + padding: 10px 0; height: 160px; } } diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 9f91091d..00bb201f 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -136,10 +136,24 @@ harvester: delete: Delete bundle: title: Generate a Support Bundle + titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support. + tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the documentation. url: Issue URL description: Description - requiredDesc: Description is required! - titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support. + namespaces: + label: Namespaces + tooltip: Select additional namespaces to include in the support bundle.
See docs support-bundle-namespaces for detail. + selectAll: 'Select All' + unselectAll: 'Unselect All' + timeout: + label: Timeout + tooltip: Minutes allows for completion of the support bundle generation process.
See docs support-bundle-timeout for detail. + expiration: + label: Expiration + tooltip: Minutes before deleting packaged but not downloaded support bundle.
See docs support-bundle-expiration for detail. + nodeTimeout: + label: Node Collection Timeout + tooltip: Minutes allowed for collecting logs/configurations on nodes.
See docs support-bundle-node-collection-timeout for detail. hotplug: success: 'Volume { diskName } is mounted to the virtual machine { vm }.' title: Add Volume @@ -1597,7 +1611,7 @@ advancedSettings: 'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down. 'harv-ssl-parameters': Custom SSL Parameters for TLS validation. 'harv-storage-network': 'Longhorn storage-network setting.' - 'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package. + 'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle. 'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage. 'harv-support-bundle-image': Support bundle image configuration. Find different versions in rancher/support-bundle-kit. 'harv-release-download-url': This setting allows you to configure the upgrade release download URL address. Harvester will get the ISO URL and checksum value from the ($URL/$VERSION/version.yaml) file hosted by the configured URL.