From 45116b568ad1756d18f3beab5ccce97e4a965482 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Wed, 16 Sep 2026 09:30:37 +0800 Subject: [PATCH] feat: change logo based on server-flavor setting (#1151) * feat: use prime logo when server-flavor is prime Signed-off-by: Andy Lee * feat: expose server-flavor setting Signed-off-by: Andy Lee * refactor: copilot review Signed-off-by: Andy Lee --------- Signed-off-by: Andy Lee --- .../images/dark/suse-virtualization.svg | 1 + .../assets/images/suse-virtualization.svg | 1 + pkg/harvester/components/SettingList.vue | 6 +- pkg/harvester/config/harvester-cluster.js | 56 ++++++++++++++++--- pkg/harvester/config/settings.ts | 2 + pkg/harvester/l10n/en-us.yaml | 3 + .../management.cattle.io.setting.js | 14 +++++ .../pages/c/_cluster/brand/index.vue | 16 +++++- pkg/harvester/store/harvester-common.js | 19 +++++++ 9 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 pkg/harvester/assets/images/dark/suse-virtualization.svg create mode 100644 pkg/harvester/assets/images/suse-virtualization.svg create mode 100644 pkg/harvester/models/management/management.cattle.io.setting.js diff --git a/pkg/harvester/assets/images/dark/suse-virtualization.svg b/pkg/harvester/assets/images/dark/suse-virtualization.svg new file mode 100644 index 00000000..3d6b2784 --- /dev/null +++ b/pkg/harvester/assets/images/dark/suse-virtualization.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/harvester/assets/images/suse-virtualization.svg b/pkg/harvester/assets/images/suse-virtualization.svg new file mode 100644 index 00000000..a1565a6f --- /dev/null +++ b/pkg/harvester/assets/images/suse-virtualization.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/harvester/components/SettingList.vue b/pkg/harvester/components/SettingList.vue index cf8713a5..85f10a78 100644 --- a/pkg/harvester/components/SettingList.vue +++ b/pkg/harvester/components/SettingList.vue @@ -10,6 +10,7 @@ import { docLink } from '../utils/feature-flags'; const CATEGORY = { ui: [ 'branding', + HCI_SETTING.SERVER_FLAVOR, 'ui-source', 'ui-index', ] @@ -53,7 +54,7 @@ export default { }; }, - computed: { ...mapGetters({ t: 'i18n/t' }) }, + computed: { ...mapGetters({ t: 'i18n/t', isHarvesterPrime: 'harvester-common/isHarvesterPrime' }) }, watch: { settings: { @@ -269,7 +270,8 @@ export default { v-if="!setting.hide" class="settings-value" > -
{{ setting.json }}
+
{{ $store.getters['harvester-common/privateLabel'] }}
+
{{ setting.json }}
{{ setting.data.value || setting.data.default }}
{{ t(setting.enum) }}
{{ setting.custom }}
diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index 53eb6ddd..7aa32da9 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -57,6 +57,8 @@ import { } from './table-headers'; import { ADD_ONS } from './harvester-map'; import { registerAddonSideNav } from '../utils/dynamic-nav'; +import { setVendor } from '@shell/config/private-label'; +import { createCssVars } from '@shell/utils/color'; const TEMPLATE = HCI.VM_VERSION; const MONITORING_GROUP = 'Monitoring & Logging::Monitoring'; @@ -109,15 +111,51 @@ export function init($plugin, store) { } }; - store.dispatch('setIsSingleProduct', { - productName: PRODUCT_NAME, - logo: require(`@shell/assets/images/providers/harvester.svg`), - productNameKey: 'harvester.productLabel', - getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown', - afterLoginRoute: home, - logoRoute: home, - supportCustomLogo: true - }); + store.watch( + (_state, getters) => JSON.stringify([ + !!getters['harvester-common/isHarvesterPrime'], + getters['prefs/theme'] === 'dark', + getters['harvester-common/privateLabel'] + ]), + (value) => { + const [isHarvesterPrime, isDark, privateLabel] = JSON.parse(value); + const primeLogo = isDark ? require('../assets/images/dark/suse-virtualization.svg') : require('../assets/images/suse-virtualization.svg'); + + setVendor(privateLabel || ''); + + let primaryColorStyle = document.getElementById('harvester-prime-primary-color'); + + // set harvester prime primary color style to #4dd192 (Cyan Green) + if (isHarvesterPrime) { + if (!primaryColorStyle) { + primaryColorStyle = document.createElement('style'); + primaryColorStyle.id = 'harvester-prime-primary-color'; + document.head.appendChild(primaryColorStyle); + primaryColorStyle.sheet.insertRule('body.theme-light, body.theme-dark {}', 0); + } + + const colors = createCssVars('#4dd192', isDark ? 'dark' : 'light'); + const rule = primaryColorStyle.sheet.cssRules[0]; + + Object.entries(colors).forEach(([property, color]) => rule.style.setProperty(property.trim(), color)); + } else { + primaryColorStyle?.remove(); + } + + const primeLabelKey = privateLabel ? 'harvester.branding.primeLabel' : 'harvester.branding.primeEmptyLabel'; + + store.dispatch('setIsSingleProduct', { + productName: PRODUCT_NAME, + logo: isHarvesterPrime ? primeLogo : require(`@shell/assets/images/providers/harvester.svg`), + productNameKey: isHarvesterPrime ? primeLabelKey : 'harvester.productLabel', + getVersionInfo: (store) => store.getters[`${ PRODUCT_NAME }/byId`]?.(HCI.SETTING, 'server-version')?.value || 'unknown', + afterLoginRoute: home, + logoRoute: home, + supportCustomLogo: !isHarvesterPrime + }); + }, + { immediate: true } + ); } product({ diff --git a/pkg/harvester/config/settings.ts b/pkg/harvester/config/settings.ts index 7b2f62bd..bc66b93a 100644 --- a/pkg/harvester/config/settings.ts +++ b/pkg/harvester/config/settings.ts @@ -3,6 +3,7 @@ export const HCI_SETTING = { CONTAINERD_REGISTRY: 'containerd-registry', LOG_LEVEL: 'log-level', SERVER_VERSION: 'server-version', + SERVER_FLAVOR: 'server-flavor', UI_INDEX: 'ui-index', UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled', UPGRADE_CHECKER_URL: 'upgrade-checker-url', @@ -68,6 +69,7 @@ export const HCI_ALLOWED_SETTINGS = { kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation' }, [HCI_SETTING.SERVER_VERSION]: { readOnly: true }, + [HCI_SETTING.SERVER_FLAVOR]: { readOnly: true }, [HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' }, [HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' }, [HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' }, diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 785378a8..cfa8e46d 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -84,6 +84,8 @@ dialog: harvester: branding: + primeLabel: '{vendor}' + primeEmptyLabel: '' logos: tip: 'Upload a logo to replace the Harvester logo in the top-level navigation header. Image height should be 21 pixels with a max width of 200 pixels. Max file size is 20KB. Accepted formats: JPEG, PNG, SVG.' favicon: @@ -2445,6 +2447,7 @@ advancedSettings: 'harv-containerd-registry': Containerd Registry Configuration to connect private registries. 'harv-log-level': Configure Harvester server log level. Defaults to Info. 'harv-server-version': Harvester server version. + 'harv-server-flavor': "Indicates the Harvester edition: Prime or Community." 'harv-upgrade-checker-enabled': Specifies whether to enable Harvester upgrade check or not. Default is True. 'harv-upgrade-checker-url': Default Harvester upgrade check url. Only used when the upgrade-checker-enabled is equal to True. 'harv-ui-source': Configure how to load the UI source. The `External` option is used to load the UI from a ui-index URL, while the `Bundled` option loads the UI from the local Harvester server. diff --git a/pkg/harvester/models/management/management.cattle.io.setting.js b/pkg/harvester/models/management/management.cattle.io.setting.js new file mode 100644 index 00000000..6ab07270 --- /dev/null +++ b/pkg/harvester/models/management/management.cattle.io.setting.js @@ -0,0 +1,14 @@ +import ManagementSetting from '@shell/models/management.cattle.io.setting'; +import { HCI_SETTING } from '../../config/settings'; + +export default class HarvesterManagementSetting extends ManagementSetting { + get _availableActions() { + const actions = super._availableActions; + + if (this.$rootGetters['isStandaloneHarvester'] && this.id === HCI_SETTING.UI_PL) { + return actions.filter((action) => !['goToClone', 'promptRemove'].includes(action.action)); + } + + return actions; + } +} diff --git a/pkg/harvester/pages/c/_cluster/brand/index.vue b/pkg/harvester/pages/c/_cluster/brand/index.vue index 047d57dc..fd493e2a 100644 --- a/pkg/harvester/pages/c/_cluster/brand/index.vue +++ b/pkg/harvester/pages/c/_cluster/brand/index.vue @@ -63,6 +63,7 @@ export default { return { vendor: getVendor(), uiPLSetting: {}, + privateLabelDraft: null, uiLogoDarkSetting: {}, uiLogoDark: '', uiLogoLightSetting: {}, @@ -81,6 +82,17 @@ export default { }; }, computed: { + isHarvesterPrime() { + return !!this.$store.getters['harvester-common/isHarvesterPrime']; + }, + privateLabel: { + get() { + return this.privateLabelDraft ?? (this.isHarvesterPrime ? this.$store.getters['harvester-common/privateLabel'] : this.uiPLSetting.value); + }, + set(value) { + this.privateLabelDraft = value; + } + }, mode() { const schema = this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING); @@ -112,7 +124,7 @@ export default { this.errors.push(e); }, async save(btnCB) { - this.uiPLSetting.value = this.uiPLSetting.value.replaceAll(/[\<>&=#()"]/gm, ''); + this.uiPLSetting.value = (this.privateLabel || '').replaceAll(/[\<>&=#()"]/gm, ''); if (this.customizeLogo) { this.uiLogoLightSetting.value = this.uiLogoLight; this.uiLogoDarkSetting.value = this.uiLogoDark; @@ -169,7 +181,7 @@ export default {
{ + const setting = rootGetters['harvester/byId']?.(HCI.SETTING, 'server-flavor'); + + return (setting?.value || setting?.default) === 'prime'; + }, + + privateLabel: (_state, getters, _rootState, rootGetters) => { + const setting = rootGetters['management/byId']?.(MANAGEMENT.SETTING, SETTING.PL); + const value = setting?.value || ''; + + if (getters.isHarvesterPrime && value === (setting?.default || 'Harvester')) { + return ''; + } + + return value; + }, + getBundleId(state) { return state.latestBundleId; },