blob: ea146df8cb3ccbf4818612c4f8a72cafc5d4a82a [file]
// 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>