mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 13:11:43 +00:00
375 lines
9.8 KiB
Vue
375 lines
9.8 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import ActionMenu from '@shell/components/ActionMenuShell';
|
|
import { Banner } from '@components/Banner';
|
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
|
import { DOC } from '../config/doc-links';
|
|
import { docLink } from '../utils/feature-flags';
|
|
|
|
const CATEGORY = {
|
|
ui: [
|
|
'branding',
|
|
'ui-source',
|
|
'ui-index',
|
|
]
|
|
};
|
|
|
|
export default {
|
|
name: 'SettingLists',
|
|
|
|
components: {
|
|
AsyncButton,
|
|
Banner,
|
|
ActionMenu
|
|
},
|
|
|
|
props: {
|
|
settings: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
|
|
category: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
searchQuery: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
|
|
data() {
|
|
const categorySettings = this.filterCategorySettings();
|
|
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
|
|
|
|
return {
|
|
HCI_SETTING,
|
|
categorySettings,
|
|
filteredSettings,
|
|
originalHideMap: this.createHideMap(categorySettings)
|
|
};
|
|
},
|
|
|
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
|
|
|
watch: {
|
|
settings: {
|
|
deep: true,
|
|
handler() {
|
|
this.categorySettings = this.filterCategorySettings();
|
|
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
|
|
}
|
|
},
|
|
searchQuery: {
|
|
immediate: true,
|
|
handler(newQuery) {
|
|
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
|
|
|
|
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
|
|
}
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
createHideMap(settings = []) {
|
|
const map = settings.reduce((acc, s) => {
|
|
acc[s.id] = s.hide ?? false;
|
|
|
|
return acc;
|
|
}, {} );
|
|
|
|
return map;
|
|
},
|
|
filterSearchSettings(settings, searchKey) {
|
|
if (!searchKey) {
|
|
return this.filterCategorySettings();
|
|
}
|
|
const searchQuery = searchKey.toLowerCase();
|
|
|
|
return settings.filter((setting) => {
|
|
const id = setting.id?.toLowerCase() || '';
|
|
|
|
// filter by id
|
|
if (id.includes(searchQuery) ) {
|
|
return true;
|
|
}
|
|
|
|
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
|
|
|
// remove <a> tags from description
|
|
if (description.includes('<a')) {
|
|
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
|
}
|
|
// filter by description
|
|
if (description.includes(searchQuery)) {
|
|
return true;
|
|
}
|
|
|
|
// filter by customized value
|
|
if (setting.customized === true && setting.data?.value) {
|
|
const value = setting.data.value?.toLowerCase() || '';
|
|
|
|
return value.includes(searchQuery);
|
|
}
|
|
|
|
// filter by json value
|
|
if (setting.kind === 'json' && setting.json) {
|
|
try {
|
|
const json = JSON.parse(setting.json);
|
|
const jsonString = JSON.stringify(json).toLowerCase();
|
|
|
|
return jsonString.includes(searchQuery);
|
|
} catch (e) {
|
|
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// filter by default value
|
|
if (setting.data?.default) {
|
|
return setting.data?.default.includes(searchQuery);
|
|
}
|
|
|
|
return false;
|
|
});
|
|
},
|
|
|
|
filterCategorySettings() {
|
|
return this.settings.filter((s) => {
|
|
if (!this.getFeatureEnabled(s.featureFlag)) {
|
|
return false;
|
|
}
|
|
|
|
if (this.category !== 'advanced') {
|
|
return (CATEGORY[this.category] || []).find((item) => item === s.id);
|
|
} else if (this.category === 'advanced') {
|
|
const allCategory = Object.keys(CATEGORY);
|
|
|
|
return !allCategory.some((category) => (CATEGORY[category] || []).find((item) => item === s.id));
|
|
}
|
|
}) || [];
|
|
},
|
|
|
|
getFeatureEnabled(id) {
|
|
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
|
},
|
|
|
|
getSettingOption(id) {
|
|
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
|
},
|
|
|
|
openJsonSettings(settings) {
|
|
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
|
|
},
|
|
|
|
toggleHide(s) {
|
|
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
|
|
|
if (setting) {
|
|
setting.hide = !setting.hide;
|
|
this.originalHideMap[setting.id] = setting.hide;
|
|
}
|
|
},
|
|
|
|
async testConnect(buttonDone, value) {
|
|
try {
|
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/backuptarget/healthz');
|
|
|
|
const health = await this.$store.dispatch('harvester/request', { url });
|
|
const settingValue = JSON.parse(value);
|
|
|
|
if (health?._status === 200) {
|
|
this.$store.dispatch('growl/success', {
|
|
title: this.t('harvester.notification.title.succeed'),
|
|
message: this.t('harvester.backup.message.testConnect.successMessage', { endpoint: settingValue?.endpoint })
|
|
}, { root: true });
|
|
}
|
|
buttonDone(true);
|
|
} catch (err) {
|
|
if (err?._status === 400 || err?._status === 503) {
|
|
this.$store.dispatch('growl/error', {
|
|
title: this.t('harvester.notification.title.error'),
|
|
message: err?.errors[0]
|
|
}, { root: true });
|
|
}
|
|
buttonDone(false);
|
|
}
|
|
},
|
|
|
|
getDocLinkParams(setting) {
|
|
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
|
|
|
if (settingConfig?.docPath) {
|
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
|
const url = docLink(DOC[settingConfig.docPath], version);
|
|
|
|
return { url };
|
|
}
|
|
|
|
return {};
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<div
|
|
v-for="(setting, i) in filteredSettings"
|
|
:key="i"
|
|
class="advanced-setting mb-20"
|
|
>
|
|
<div class="header">
|
|
<div class="title">
|
|
<h1>
|
|
{{ setting.id }}
|
|
<span
|
|
v-if="setting.customized"
|
|
class="modified"
|
|
>
|
|
Modified
|
|
</span>
|
|
<span
|
|
v-if="setting.experimental"
|
|
v-clean-tooltip="t('advancedSettings.experimental')"
|
|
class="experimental"
|
|
>
|
|
Experimental
|
|
</span>
|
|
</h1>
|
|
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
|
</h2>
|
|
</div>
|
|
<div
|
|
v-if="setting.hasActions"
|
|
:id="setting.id"
|
|
class="action"
|
|
>
|
|
<ActionMenu
|
|
:resource="setting.data"
|
|
:button-aria-label="t('advancedSettings.edit.label')"
|
|
data-testid="action-button"
|
|
button-role="tertiary"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div value>
|
|
<div
|
|
v-if="!setting.hide"
|
|
class="settings-value"
|
|
>
|
|
<pre v-if="setting.kind === 'json'">{{ setting.json }}</pre>
|
|
<pre v-else-if="setting.kind === 'multiline'">{{ setting.data.value || setting.data.default }}</pre>
|
|
<pre v-else-if="setting.kind === 'enum'">{{ t(setting.enum) }}</pre>
|
|
<pre v-else-if="setting.kind === 'custom' && setting.custom">{{ setting.custom }}</pre>
|
|
<pre v-else-if="setting.data.value || setting.data.default">{{ setting.data.value || setting.data.default }}</pre>
|
|
<pre
|
|
v-else
|
|
class="text-muted"
|
|
><{{ t('advancedSettings.none') }}></pre>
|
|
</div>
|
|
|
|
<div class="mt-5">
|
|
<button
|
|
v-if="setting.hide"
|
|
class="btn btn-sm role-primary"
|
|
@click="toggleHide(setting)"
|
|
>
|
|
{{ t('advancedSettings.show') }} {{ setting.id }}
|
|
</button>
|
|
|
|
<button
|
|
v-if="setting.canHide && !setting.hide"
|
|
class="btn btn-sm role-primary"
|
|
@click="toggleHide(setting)"
|
|
>
|
|
{{ t('advancedSettings.hide') }} {{ setting.id }}
|
|
</button>
|
|
|
|
<AsyncButton
|
|
v-if="setting.id === HCI_SETTING.BACKUP_TARGET"
|
|
class="backupButton ml-5"
|
|
mode="apply"
|
|
size="sm"
|
|
:delay="0"
|
|
:action-label="t('harvester.backup.message.testConnect.actionLabel')"
|
|
:waiting-label="t('harvester.backup.message.testConnect.waitingLabel')"
|
|
:success-label="t('harvester.backup.message.testConnect.successLabel')"
|
|
@click="(buttonCb) => testConnect(buttonCb, setting.data.value)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Banner
|
|
v-if="setting.data.errMessage"
|
|
color="error mt-5"
|
|
class="settings-banner"
|
|
>
|
|
{{ setting.data.errMessage }}
|
|
</Banner>
|
|
</div>
|
|
<div
|
|
v-if="filteredSettings.length === 0"
|
|
class="advanced-setting mb-20 no-search-match"
|
|
>
|
|
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang='scss' scoped>
|
|
.settings-banner {
|
|
margin-top: 0;
|
|
}
|
|
.advanced-setting {
|
|
border: 1px solid var(--border);
|
|
padding: 20px;
|
|
border-radius: var(--border-radius);
|
|
|
|
h1 {
|
|
font-size: 14px;
|
|
}
|
|
h2 {
|
|
font-size: 12px;
|
|
margin-bottom: 0;
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
|
|
.settings-value pre {
|
|
margin: 0;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.title {
|
|
flex: 1;
|
|
}
|
|
|
|
.modified {
|
|
margin-left: 10px;
|
|
border: 1px solid var(--primary);
|
|
border-radius: 5px;
|
|
padding: 2px 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.experimental {
|
|
margin-left: 10px;
|
|
border: 1px solid var(--error);
|
|
border-radius: 5px;
|
|
padding: 2px 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.no-search-match {
|
|
text-align: center;
|
|
}
|
|
</style>
|