blob: de40fc9908dc6c93b5f95291100cd4094a7c279f [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>
<div>
<a-table
size="small"
:columns="columns"
:dataSource="rules"
:rowKey="item => item.id"
:pagination="false"
:loading="tabLoading"
:scroll="{ x: readOnly ? 700 : 950 }">
<template #bodyCell="{ column, text, record }">
<template v-if="column.key === 'ruletype'">
{{ $t('label.' + text.toLowerCase()) }}
<a-tag v-if="record.inherited" color="blue">{{ $t('label.inherited') }}</a-tag>
</template>
<template v-if="column.key === 'name'">
<a-input v-if="editingRuleId === record.id" v-model:value="editForm.name" size="small" />
<span v-else>{{ text }}</span>
</template>
<template v-if="column.key === 'details'">
<template v-if="detailEntries(record).length">
<a-tag v-for="entry in detailEntries(record)" :key="entry.key">{{ entry.key }}: {{ entry.value }}</a-tag>
</template>
<span v-else>-</span>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="statusColor(text)">{{ text || 'UNKNOWN' }}</a-tag>
</template>
<template v-if="column.key === 'enabled'">
<a-switch v-if="editingRuleId === record.id" v-model:checked="editForm.enabled" size="small" />
<status v-else :text="text ? 'true' : 'false'" displayText />
</template>
<template v-if="column.key === 'statusmessage'">
<a-tooltip :title="text">
<div class="readiness-rule-message">{{ text }}</div>
</a-tooltip>
<div v-if="record.checkedon" class="readiness-rule-checkedon">{{ $t('label.last.checked') }}: {{ $toLocaleDate(record.checkedon) }}</div>
</template>
<template v-if="column.key === 'actions'">
<template v-if="editingRuleId === record.id">
<tooltip-button
key="save"
:tooltip="$t('label.save')"
type="primary"
icon="check-outlined"
style="margin-right: 5px"
:loading="editSubmitLoading"
@click="saveEditRule(record)" />
<tooltip-button
key="cancel"
:tooltip="$t('label.cancel')"
icon="close-outlined"
@click="cancelEditRule" />
</template>
<template v-else-if="record.inherited">
<tooltip-button
key="inherited"
:tooltip="$t('message.inherited.readiness.rule')"
:disabled="true"
icon="delete-outlined" />
</template>
<template v-else>
<tooltip-button
key="edit"
:tooltip="$t('label.edit.readiness.rule')"
icon="edit-outlined"
style="margin-right: 5px"
:disabled="editingRuleId !== null"
@click="startEditRule(record)" />
<a-popconfirm
key="delete"
placement="topRight"
:title="$t('message.confirm.delete.readiness.rule')"
:ok-text="$t('label.yes')"
:cancel-text="$t('label.no')"
@confirm="deleteRule(record)">
<tooltip-button
:tooltip="$t('label.delete')"
type="primary"
:danger="true"
:disabled="editingRuleId !== null"
icon="delete-outlined" />
</a-popconfirm>
</template>
</template>
</template>
</a-table>
<template v-if="!readOnly">
<a-divider />
<a-form
v-if="availableRuleTypes.length"
:ref="formRef"
:model="form"
:rules="formRules"
layout="vertical"
@finish="handleSubmit">
<a-form-item name="ruletype" ref="ruletype">
<template #label>
<tooltip-label :title="$t('label.rule.type')" :tooltip="apiParams.ruletype.description"/>
</template>
<span v-if="isGuestLivenessSelected">
<a-alert type="warning">
<template #message>
<span v-html="$t('message.readiness.guest.liveness.warning')" />
</template>
</a-alert>
<br/>
</span>
<a-select v-model:value="form.ruletype" @change="onRuleTypeChange">
<a-select-option v-for="type in availableRuleTypes" :key="type" :value="type">{{ $t('label.' + type.toLowerCase()) }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description"/>
</template>
<a-input v-model:value="form.name" :placeholder="apiParams.name.description" />
</a-form-item>
<template v-if="form.ruletype === 'PortCheck'">
<a-form-item name="port" ref="port" :label="$t('label.port')">
<a-input-number v-model:value="form.port" style="width: 100%" :min="1" :max="65535" />
</a-form-item>
<a-form-item name="protocol" ref="protocol" :label="$t('label.protocol')">
<a-select v-model:value="form.protocol">
<a-select-option value="tcp">tcp</a-select-option>
</a-select>
</a-form-item>
</template>
<template v-if="form.ruletype === 'MemberQuorum'">
<a-form-item name="thresholdtype" ref="thresholdtype" :label="$t('label.threshold.type')">
<a-select v-model:value="form.thresholdtype">
<a-select-option value="COUNT">COUNT</a-select-option>
<a-select-option value="PERCENTAGE">PERCENTAGE</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="thresholdvalue" ref="thresholdvalue" :label="$t('label.threshold.value')">
<a-input-number v-model:value="form.thresholdvalue" style="width: 100%" :min="0" />
</a-form-item>
</template>
<div :span="24" class="action-button">
<a-button :loading="submitLoading" type="primary" @click="handleSubmit">{{ $t('label.add.readiness.rule') }}</a-button>
</div>
</a-form>
<a-alert v-else type="info" :message="$t('message.no.more.readiness.rule.types')" show-icon />
</template>
</div>
</template>
<script>
import { ref, reactive } from 'vue'
import { getAPI, postAPI } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import Status from '@/components/widgets/Status'
export default {
name: 'InstanceBootGroupReadinessRulesModal',
components: {
TooltipButton,
TooltipLabel,
Status
},
props: {
bootGroupId: {
type: String,
required: true
},
itemType: {
type: String,
required: true
},
itemId: {
type: String,
required: true
},
/** Drops the delete action and add-rule form, leaving just the rule/status table. */
readOnly: {
type: Boolean,
default: false
}
},
data () {
return {
tabLoading: false,
submitLoading: false,
rules: [],
editingRuleId: null,
editForm: { name: '', enabled: true },
editSubmitLoading: false
}
},
computed: {
columns () {
const columns = [
{ key: 'ruletype', title: this.$t('label.rule.type'), dataIndex: 'ruletype', width: 140 },
{ key: 'name', title: this.$t('label.name'), dataIndex: 'name', width: 120 },
{ key: 'details', title: this.$t('label.details'), dataIndex: 'details', width: 180 },
{ key: 'enabled', title: this.$t('label.enabled'), dataIndex: 'enabled', width: 90 },
{ key: 'status', title: this.$t('label.status'), dataIndex: 'readinessstatus', width: 100 },
{ key: 'statusmessage', title: this.$t('label.message'), dataIndex: 'statusmessage', width: 220 }
]
if (!this.readOnly) {
columns.push({ key: 'actions', title: this.$t('label.actions'), width: 130 })
}
return columns
},
/**
* Ping/GuestAgentLiveness are singleton regardless of item type; MemberQuorum is singleton but
* only applies to an InstanceGroup. PortCheck allows multiple (different ports).
*/
singletonRuleTypes () {
return this.itemType === 'VirtualMachine' ? ['Ping', 'GuestAgentLiveness'] : ['Ping', 'GuestAgentLiveness', 'MemberQuorum']
},
availableRuleTypes () {
const allRuleTypes = this.itemType === 'VirtualMachine'
? ['Ping', 'GuestAgentLiveness', 'PortCheck']
: ['Ping', 'GuestAgentLiveness', 'PortCheck', 'MemberQuorum']
const existingRuleTypes = this.rules.map(rule => rule.ruletype)
return allRuleTypes.filter(type => !this.singletonRuleTypes.includes(type) || !existingRuleTypes.includes(type))
},
isGuestLivenessSelected () {
return this.form.ruletype === 'GuestAgentLiveness'
}
},
beforeCreate () {
this.apiParams = this.$getApiParams('createInstanceBootGroupReadinessRule')
},
created () {
this.initForm()
this.fetchRules()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({
ruletype: this.availableRuleTypes[0],
name: undefined,
port: 80,
protocol: 'tcp',
thresholdtype: 'PERCENTAGE',
thresholdvalue: 100
})
this.formRules = reactive({
ruletype: [{ required: true, message: this.$t('message.error.select') }]
})
},
/** No-op: the conditional fields are driven directly off form.ruletype. */
onRuleTypeChange () {
},
fetchRules () {
this.tabLoading = true
const params = { bootgroupid: this.bootGroupId }
if (this.itemType === 'VirtualMachine') {
params.virtualmachineid = this.itemId
} else {
params.instancegroupid = this.itemId
}
getAPI('listInstanceBootGroupReadinessRules', params).then(json => {
this.rules = json?.listinstancebootgroupreadinessrulesresponse?.instancebootgroupreadinessrule || []
if (!this.availableRuleTypes.includes(this.form.ruletype)) {
this.form.ruletype = this.availableRuleTypes[0]
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.tabLoading = false
})
},
/**
* Collapses MemberQuorum's threshold pair and PortCheck's port/protocol into a single tag each;
* anything else falls back to one tag per raw detail key.
*/
detailEntries (record) {
const details = record.details || {}
if (record.ruletype === 'MemberQuorum' && details.threshold_type) {
return [{ key: details.threshold_type.toUpperCase(), value: details.threshold_value }]
}
if (record.ruletype === 'PortCheck' && details.port) {
return [{ key: 'PORT', value: `${details.port}/${(details.protocol || 'tcp').toUpperCase()}` }]
}
return Object.keys(details).map(key => ({ key: key.toUpperCase(), value: details[key] }))
},
statusColor (status) {
switch (status) {
case 'Ready': return 'success'
case 'NotReady': return 'warning'
case 'Error': return 'error'
default: return 'default'
}
},
handleSubmit (e) {
if (e && e.preventDefault) e.preventDefault()
if (this.submitLoading) return
this.formRef.value.validate().then(() => {
this.submitLoading = true
const params = {
bootgroupid: this.bootGroupId,
ruletype: this.form.ruletype
}
if (this.itemType === 'VirtualMachine') {
params.virtualmachineid = this.itemId
} else {
params.instancegroupid = this.itemId
}
if (this.form.name) {
params.name = this.form.name
}
const details = {}
if (this.form.ruletype === 'PortCheck') {
details.port = this.form.port
details.protocol = this.form.protocol
} else if (this.form.ruletype === 'MemberQuorum') {
details.threshold_type = this.form.thresholdtype
details.threshold_value = this.form.thresholdvalue
}
Object.keys(details).forEach((key) => {
params[`details[0].${key}`] = String(details[key])
})
postAPI('createInstanceBootGroupReadinessRule', params).then(() => {
this.$notification.success({
message: this.$t('label.add.readiness.rule'),
description: this.$t('message.success.add.readiness.rule')
})
this.fetchRules()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.submitLoading = false
})
}).catch(error => {
this.$notifyError(error)
})
},
deleteRule (record) {
postAPI('deleteInstanceBootGroupReadinessRule', { id: record.id }).then(() => {
this.$notification.success({
message: this.$t('label.delete'),
description: this.$t('message.success.delete.readiness.rule')
})
this.fetchRules()
}).catch(error => {
this.$notifyError(error)
})
},
/**
* Only name/enabled are editable — rule type and details are immutable after creation and are
* instead changed by removing and re-adding the rule. Edited in place in the table row.
*/
startEditRule (record) {
this.editingRuleId = record.id
this.editForm.name = record.name
this.editForm.enabled = record.enabled
},
cancelEditRule () {
this.editingRuleId = null
},
saveEditRule (record) {
if (this.editSubmitLoading) return
this.editSubmitLoading = true
postAPI('updateInstanceBootGroupReadinessRule', {
id: record.id,
name: this.editForm.name,
enabled: this.editForm.enabled
}).then(() => {
this.$notification.success({
message: this.$t('label.edit.readiness.rule'),
description: this.$t('message.success.edit.readiness.rule')
})
this.editingRuleId = null
this.fetchRules()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.editSubmitLoading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.readiness-rule-message {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.readiness-rule-checkedon {
font-size: 11px;
color: rgba(0, 0, 0, 0.45);
white-space: nowrap;
}
</style>