| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| <template> |
| <a-list> |
| <a-list-item> |
| <span v-if="configrecord.type ==='Boolean'"> |
| <a-tooltip :title="editableValue?'true':'false'"> |
| <a-switch |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:checked="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)" |
| /> |
| </a-tooltip> |
| </span> |
| <span v-else-if="configrecord.type ==='Number'"> |
| <a-tooltip :title="editableValue"> |
| <a-input-number |
| style="width: 20vw;" |
| :defaultValue="actualValue" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)" |
| @keydown="e => handleInputNumberKeyDown(e, false)" |
| /> |
| </a-tooltip> |
| </span> |
| <span v-else-if="configrecord.type ==='Decimal'"> |
| <a-tooltip :title="editableValue"> |
| <a-input-number |
| style="width: 20vw" |
| :defaultValue="actualValue" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)" |
| @keydown="e => handleInputNumberKeyDown(e, true)" |
| /> |
| </a-tooltip> |
| </span> |
| <span v-else-if="configrecord.type ==='Range'" style="width: 75%;"> |
| <a-row type="flex"> |
| <a-col flex="auto"> |
| <a-tooltip :title="editableValue"> |
| <a-slider |
| style="width: 13vw" |
| class="config-slider-value" |
| :defaultValue="configrecord.value * 100" |
| :min="0" |
| :max="100" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)" |
| /> |
| </a-tooltip> |
| </a-col> |
| <a-col flex="30px"> |
| <a-tooltip :title="editableValue"> |
| <a-input-number |
| style="margin-left: 10px;" |
| class="config-slider-text" |
| :defaultValue="configrecord.value * 100" |
| :min="0" |
| :max="100" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| :formatter="(value) => `${value}%`" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)" |
| @keydown="e => handleInputNumberKeyDown(e, true)" |
| /> |
| </a-tooltip> |
| </a-col> |
| </a-row> |
| </span> |
| <span v-else-if="configrecord.type === 'Select'"> |
| <a-tooltip :title="editableValue"> |
| <a-select |
| style="width: 20vw" |
| :defaultValue="actualValue" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)"> |
| <a-select-option |
| v-for="value of configrecord.options.split(',')" |
| :key="value"> |
| {{ value }} |
| </a-select-option> |
| </a-select> |
| </a-tooltip> |
| </span> |
| <span v-else-if="configrecord.type === 'Order' || configrecord.type === 'WhitespaceSeparatedListWithOptions'"> |
| <a-tooltip :title="editableValue.join(', ')"> |
| <b v-if="configrecord.type === 'Order'"> |
| {{ $t('message.select.deselect.to.sort') }} |
| </b> |
| <b v-else> |
| {{ $t('message.select.deselect.desired.options') }} |
| </b> |
| <br /> |
| <a-select |
| style="width: 20vw" |
| mode="multiple" |
| :defaultValue="actualValue" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)"> |
| <a-select-option |
| v-for="value of configrecord.options.split(',')" |
| :key="value"> |
| {{ value }} |
| </a-select-option> |
| </a-select> |
| </a-tooltip> |
| </span> |
| <span v-else-if="configrecord.type === 'CSV'"> |
| <a-tooltip :title="editableValue.join(', ')"> |
| <b>{{ $t('message.type.values.to.add') }}</b> |
| <br /> |
| <a-select |
| style="width: 20vw" |
| mode="tags" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)"> |
| </a-select> |
| </a-tooltip> |
| </span> |
| <span v-else> |
| <a-tooltip :title="editableValue"> |
| <a-textarea |
| style="width: 20vw; word-break: break-all" |
| :defaultValue="actualValue" |
| :disabled="(!('updateConfiguration' in $store.getters.apis) || configDisabled)" |
| v-model:value="editableValue" |
| @keydown.esc="editableValueKey = null" |
| @pressEnter="updateConfigurationValue(configrecord)" |
| @change="value => setConfigurationEditable(configrecord, value)" |
| /> |
| </a-tooltip> |
| </span> |
| <span class="actions"> |
| <tooltip-button |
| :tooltip="$t('label.cancel')" |
| @onClick="cancelEditConfigurationValue(configrecord)" |
| v-if="editableValueKey !== null" |
| iconType="CloseCircleTwoTone" |
| iconTwoToneColor="#f5222d" |
| :disabled="valueLoading" /> |
| <tooltip-button |
| :tooltip="$t('label.ok')" |
| @onClick="updateConfigurationValue(configrecord)" |
| v-if="editableValueKey !== null" |
| iconType="CheckCircleTwoTone" |
| iconTwoToneColor="#52c41a" |
| :disabled="valueLoading" /> |
| <tooltip-button |
| :tooltip="$t('label.reset.config.value')" |
| @onClick="$resetConfigurationValueConfirm(configrecord, resetConfigurationValue)" |
| v-if="editableValueKey === null" |
| icon="reload-outlined" |
| :disabled="(!('resetConfiguration' in $store.getters.apis) || configDisabled || valueLoading || configrecord.value === configrecord.defaultvalue)" /> |
| </span> |
| </a-list-item> |
| <a-modal |
| v-if="!suppressRelatedPrompt" |
| v-model:visible="relatedModalVisible" |
| :title="$t('label.related.settings')" |
| :footer="null" |
| :maskClosable="false" |
| :width="'60vw'" |
| @cancel="closeRelatedModal"> |
| <p>{{ $t('message.related.settings.changed').replace('%x', relatedSourceConfigName) }}</p> |
| <a-table |
| size="small" |
| :showHeader="false" |
| :pagination="false" |
| :loading="relatedLoading" |
| :columns="relatedColumns" |
| :dataSource="relatedConfigs" |
| :rowKey="record => record.name"> |
| <template #bodyCell="{ column, record }"> |
| <template v-if="column.key === 'name'"> |
| <b>{{ record.displaytext }}</b> {{ ' (' + record.name + ')' }} <br/> {{ record.description }} |
| </template> |
| <template v-if="column.key === 'value'"> |
| <ConfigurationValue |
| :configrecord="record" |
| :resource="resource" |
| :suppressRelatedPrompt="true" |
| @refresh="handleRelatedConfigRefresh" /> |
| </template> |
| </template> |
| </a-table> |
| </a-modal> |
| </a-list> |
| </template> |
| <script> |
| import { getAPI, postAPI } from '@/api' |
| import TooltipButton from '@/components/widgets/TooltipButton' |
| |
| export default { |
| name: 'ConfigurationValue', |
| components: { |
| TooltipButton |
| }, |
| props: { |
| configrecord: { |
| type: Object, |
| required: true |
| }, |
| loading: { |
| type: Boolean, |
| default: false |
| }, |
| configDisabled: { |
| type: Boolean, |
| default: false |
| }, |
| actions: { |
| type: Array, |
| default: () => [] |
| }, |
| resource: { |
| type: Object, |
| required: false |
| }, |
| suppressRelatedPrompt: { |
| type: Boolean, |
| default: false |
| } |
| }, |
| data () { |
| return { |
| valueLoading: this.loading, |
| scopeKey: '', |
| actualValue: null, |
| editableValue: null, |
| editableValueKey: null, |
| relatedModalVisible: false, |
| relatedLoading: false, |
| relatedConfigs: [], |
| relatedSourceConfigName: '', |
| relatedColumns: [ |
| { |
| title: 'name', |
| dataIndex: 'name', |
| key: 'name' |
| }, |
| { |
| title: 'value', |
| dataIndex: 'value', |
| key: 'value', |
| width: '35%' |
| } |
| ] |
| } |
| }, |
| created () { |
| switch (this.$route.meta.name) { |
| case 'account': |
| this.scopeKey = 'accountid' |
| break |
| case 'domain': |
| this.scopeKey = 'domainid' |
| break |
| case 'zone': |
| this.scopeKey = 'zoneid' |
| break |
| case 'cluster': |
| this.scopeKey = 'clusterid' |
| break |
| case 'storagepool': |
| this.scopeKey = 'storageid' |
| break |
| case 'imagestore': |
| this.scopeKey = 'imagestoreuuid' |
| break |
| default: |
| this.scopeKey = '' |
| } |
| this.setConfigData() |
| }, |
| watch: { |
| configrecord: { |
| handler (newRecord) { |
| this.setConfigData() |
| if (newRecord && newRecord.type === 'Boolean' && !this.suppressRelatedPrompt && |
| this.$route.meta.name === 'globalsetting' && this.isConfigNowActive(newRecord)) { |
| this.fetchRelatedConfigurations(newRecord) |
| } |
| }, |
| deep: true |
| } |
| }, |
| methods: { |
| setConfigData () { |
| this.valueLoading = false |
| this.editableValue = this.getEditableValue(this.configrecord) |
| this.actualValue = this.editableValue |
| this.editableValueKey = null |
| }, |
| updateConfigurationValue (configrecord) { |
| let configRecordEntry = this.configrecord |
| this.valueLoading = true |
| this.editableValueKey = null |
| var newValue = this.editableValue |
| if (configrecord.type === 'Range') { |
| newValue = newValue / 100 |
| } |
| if (['Order', 'CSV'].includes(configrecord.type)) { |
| newValue = newValue.join(',') |
| } |
| if (configrecord.type === 'WhitespaceSeparatedListWithOptions') { |
| newValue = newValue.join(' ') |
| } |
| const params = { |
| [this.scopeKey]: this.$route.params?.id, |
| name: configrecord.name, |
| value: newValue |
| } |
| if (this.scopeKey === 'domainid' && !params[this.scopeKey]) { |
| params[this.scopeKey] = this.resource?.id |
| } |
| postAPI('updateConfiguration', params).then(json => { |
| // The updateConfiguration response can return a stale value for |
| // non-global scopes (server-side issue). Trust the value the user |
| // just submitted — the API returned success, so we know it was persisted. |
| const apiRecord = json.updateconfigurationresponse.configuration |
| configRecordEntry = { ...apiRecord, value: String(newValue) } |
| this.editableValue = this.getEditableValue(configRecordEntry) |
| this.actualValue = this.editableValue |
| this.$emit('change-config', { value: newValue }) |
| this.$store.dispatch('RefreshFeatures') |
| this.$messageConfigSuccess(`${this.$t('message.setting.updated')} ${configrecord.name}`, configrecord) |
| this.$notifyConfigurationValueChange(configRecordEntry) |
| }).catch(error => { |
| this.editableValue = this.actualValue |
| console.error(error) |
| this.$message.error(this.$t('message.error.save.setting')) |
| this.$notification.error({ |
| message: this.$t('label.error'), |
| description: error?.response?.data?.updateconfigurationresponse?.errortext || this.$t('message.error.save.setting') |
| }) |
| }).finally(() => { |
| this.valueLoading = false |
| this.$emit('refresh', configrecord.name, configRecordEntry) |
| }) |
| }, |
| isConfigNowActive (configrecord) { |
| const name = configrecord.name || '' |
| if (name.endsWith('.disabled')) { |
| return configrecord.value === 'false' |
| } |
| return configrecord.value === 'true' |
| }, |
| getRelatedConfigPrefix (configrecord) { |
| const name = configrecord.name || '' |
| const segments = name.split('.') |
| if (segments.length < 2) { |
| return null |
| } |
| let prefixSegments = segments.slice(0, -1) |
| if ((name.endsWith('.service.enabled') || name.endsWith('.service.disabled')) && prefixSegments.length > 1) { |
| prefixSegments = prefixSegments.slice(0, -1) |
| } |
| return prefixSegments.join('.') |
| }, |
| fetchRelatedConfigurations (configrecord) { |
| const prefix = this.getRelatedConfigPrefix(configrecord) |
| if (!prefix) { |
| return |
| } |
| this.relatedLoading = true |
| const params = { |
| [this.scopeKey]: this.$route.params?.id, |
| keyword: prefix, |
| pagesize: -1, |
| listAll: true |
| } |
| if (this.scopeKey === 'domainid' && !params[this.scopeKey]) { |
| params[this.scopeKey] = this.resource?.id |
| } |
| getAPI('listConfigurations', params).then(json => { |
| const list = json?.listconfigurationsresponse?.configuration || [] |
| this.relatedConfigs = list.filter(c => c.name !== configrecord.name && c.name.startsWith(prefix + '.')) |
| if (this.relatedConfigs.length > 0) { |
| this.relatedSourceConfigName = configrecord.name |
| this.relatedModalVisible = true |
| } |
| }).catch(error => { |
| console.error(error) |
| }).finally(() => { |
| this.relatedLoading = false |
| }) |
| }, |
| handleRelatedConfigRefresh (name, updatedRecord) { |
| if (!name || !updatedRecord) return |
| const index = this.relatedConfigs.findIndex(item => item.name === name) |
| if (index !== -1) { |
| this.relatedConfigs.splice(index, 1, updatedRecord) |
| } |
| this.$emit('refresh', name, updatedRecord) |
| }, |
| closeRelatedModal () { |
| this.relatedModalVisible = false |
| this.relatedConfigs = [] |
| this.relatedSourceConfigName = '' |
| }, |
| resetConfigurationValue (configrecord) { |
| let configRecordEntry = this.configrecord |
| this.valueLoading = true |
| this.editableValueKey = null |
| const params = { |
| [this.scopeKey]: this.$route.params?.id, |
| name: configrecord.name |
| } |
| if (this.scopeKey === 'domainid' && !params[this.scopeKey]) { |
| params[this.scopeKey] = this.resource?.id |
| } |
| postAPI('resetConfiguration', params).then(json => { |
| configRecordEntry = json.resetconfigurationresponse.configuration |
| this.editableValue = this.getEditableValue(configRecordEntry) |
| this.actualValue = this.editableValue |
| var newValue = this.editableValue |
| if (configrecord.type === 'Range') { |
| newValue = newValue / 100 |
| } |
| this.$emit('change-config', { value: newValue }) |
| this.$store.dispatch('RefreshFeatures') |
| this.$messageConfigSuccess(`${this.$t('label.setting')} ${configrecord.name} ${this.$t('label.reset.config.value')}`, configrecord) |
| this.$notifyConfigurationValueChange(json?.resetconfigurationresponse?.configuration || null) |
| }).catch(error => { |
| this.editableValue = this.actualValue |
| console.error(error) |
| this.$message.error(this.$t('message.error.reset.config')) |
| this.$notification.error({ |
| message: this.$t('label.error'), |
| description: this.$t('message.error.reset.config') |
| }) |
| }).finally(() => { |
| this.valueLoading = false |
| this.$emit('refresh', configrecord.name, configRecordEntry) |
| }) |
| }, |
| getEditableValue (configrecord) { |
| if (configrecord.type === 'Range') { |
| return Number(configrecord.value) * 100 || 0 |
| } |
| if (configrecord.type === 'Boolean') { |
| return configrecord.value === 'true' |
| } |
| if (configrecord.type === 'Number' || configrecord.type === 'Decimal') { |
| if (configrecord.value) { |
| return Number(configrecord.value) |
| } else if (configrecord.defaultvalue) { |
| return Number(configrecord.defaultvalue) |
| } |
| return 0 |
| } |
| if (['Order', 'CSV'].includes(configrecord.type)) { |
| if (configrecord.value && configrecord.value.length > 0) { |
| return String(configrecord.value).split(',') |
| } else { |
| return [] |
| } |
| } |
| if (configrecord.type === 'WhitespaceSeparatedListWithOptions') { |
| if (configrecord.value && configrecord.value.length > 0) { |
| return String(configrecord.value).split(' ') |
| } |
| |
| return [] |
| } |
| if (configrecord.value) { |
| return String(configrecord.value) |
| } |
| if (configrecord.defaultvalue) { |
| return String(configrecord.defaultvalue) |
| } |
| return '' |
| }, |
| cancelEditConfigurationValue (configrecord) { |
| this.editableValueKey = null |
| this.editableValue = this.getEditableValue(configrecord) |
| }, |
| setConfigurationEditable (configrecord, value) { |
| if (this.actualValue !== this.editableValue) { |
| this.editableValueKey = 'edit' |
| } else { |
| this.editableValueKey = null |
| } |
| }, |
| handleInputNumberKeyDown (event, isDecimal) { |
| const allowedCodes = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Minus'] |
| |
| if (isDecimal) { |
| allowedCodes.push('Period') |
| } |
| |
| if ( |
| event.getModifierState('Control') || |
| event.getModifierState('Meta') || |
| event.getModifierState('Alt') |
| ) { |
| return |
| } |
| |
| const isValid = allowedCodes.includes(event.code) || !isNaN(event.key) |
| if (!isValid) { |
| event.preventDefault() |
| } |
| } |
| } |
| } |
| </script> |
| |
| <style lang="scss" scoped> |
| .actions { |
| margin-left: 10px; |
| width: 100px; |
| } |
| </style> |