| // 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> |