feat: adapt migration queue aggregate APIs
This commit is contained in:
@@ -557,6 +557,18 @@ export const abortDataMigrate = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取跨服务数据迁移聚合队列 */
|
||||
export const getDataMigrateQueue = (params) => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/queue', { params })
|
||||
}
|
||||
|
||||
/** 按迁移记录中断队列任务 */
|
||||
export const abortDataMigrateQueue = (data) => {
|
||||
return http2.post('/api/v1/admin/server/host_service/point/vm/data_migrate/queue/abort', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取跨服务数据迁移并发及队列状态 */
|
||||
export const getDataMigrateConcurrency = () => {
|
||||
return http2.get('/api/v1/admin/server/host_service/point/vm/data_migrate/concurrency')
|
||||
|
||||
@@ -191,6 +191,7 @@ const serviceInfo = ref({})
|
||||
|
||||
// Tab 管理
|
||||
const activeTab = ref('host')
|
||||
provide('activeKvmDetailTab', activeTab)
|
||||
const tabLoaded = reactive({
|
||||
'host': true,
|
||||
'image': false,
|
||||
|
||||
@@ -33,14 +33,14 @@
|
||||
<el-table :data="queueRecords" v-loading="loading" stripe>
|
||||
<el-table-column label="队列" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.queue_position > 0" type="info">#{{ row.queue_position }}</el-tag>
|
||||
<el-tag v-if="row.global_queue_position > 0" type="info">#{{ row.global_queue_position }}</el-tag>
|
||||
<el-tag v-else type="success">执行中</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="记录 / 状态" min-width="145">
|
||||
<template #default="{ row }">
|
||||
<div class="stack-cell">
|
||||
<span>记录 #{{ row.id }}</span>
|
||||
<span>记录 #{{ row.migration_record_id }}</span>
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
@@ -83,15 +83,26 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.started_at || row.CreatedAt) }}</template>
|
||||
<template #default="{ row }">{{ formatTime(row.started_at || row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" :loading="abortingId === row.id" :disabled="row.status === 'aborting'" @click="abortMigration(row)">中断</el-button>
|
||||
<el-button link type="danger" :loading="abortingId === row.migration_record_id" :disabled="row.status === 'aborting'" @click="abortMigration(row)">中断</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && !queueRecords.length" description="当前服务暂无迁移中或排队中的虚拟机" />
|
||||
<el-pagination
|
||||
v-if="total > pageSize"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
class="queue-pagination"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="concurrencyVisible" title="迁移队列并发设置" width="480px" destroy-on-close>
|
||||
@@ -114,29 +125,30 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, inject, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh, Setting } from '@element-plus/icons-vue'
|
||||
import {
|
||||
abortDataMigrate,
|
||||
getDataMigrateConcurrency,
|
||||
getDataMigrateProgress,
|
||||
getVmMigrationRecordList,
|
||||
abortDataMigrateQueue,
|
||||
getDataMigrateQueue,
|
||||
updateDataMigrateConcurrency
|
||||
} from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
|
||||
const router = useRouter()
|
||||
const injectedServiceId = inject('serviceId', null)
|
||||
const activeDetailTab = inject('activeKvmDetailTab', null)
|
||||
const serviceId = computed(() => Number(injectedServiceId?.value) || 0)
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const queueRecords = ref([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const revision = ref(0)
|
||||
const abortingId = ref(null)
|
||||
const concurrency = reactive({ concurrency: 1, active_count: 0, queued_count: 0, available_slots: 0, updated_at: '' })
|
||||
const activeStatuses = new Set(['pending', 'starting', 'exporting', 'importing', 'running', 'aborting', 'recovering'])
|
||||
const listQueryStatuses = ['pending', 'exporting', 'importing', 'running']
|
||||
let refreshTimer = null
|
||||
let requestInFlight = false
|
||||
|
||||
@@ -160,53 +172,22 @@ const formatTime = value => {
|
||||
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
const fetchDirectionRecords = async (direction, status) => {
|
||||
const result = []
|
||||
let page = 1
|
||||
let total = 0
|
||||
do {
|
||||
const params = { page, count: 100, migration_type: 'data_migrate', status, [`${direction}_service_id`]: serviceId.value }
|
||||
const res = await getVmMigrationRecordList(params)
|
||||
if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '加载迁移队列失败'))
|
||||
const data = res.data.data || {}
|
||||
const rows = Array.isArray(data.data) ? data.data : []
|
||||
result.push(...rows)
|
||||
total = Number(data.all_count) || rows.length
|
||||
page += 1
|
||||
} while (result.length < total && page <= 20)
|
||||
return result
|
||||
}
|
||||
|
||||
const loadData = async ({ silent = false } = {}) => {
|
||||
if (!serviceId.value || requestInFlight) return
|
||||
requestInFlight = true
|
||||
if (!silent) loading.value = true
|
||||
loadError.value = ''
|
||||
try {
|
||||
const concurrencyRes = await getDataMigrateConcurrency()
|
||||
if (concurrencyRes?.data?.code !== 200) throw new Error(extractApiError(concurrencyRes?.data, '加载并发状态失败'))
|
||||
Object.assign(concurrency, concurrencyRes.data.data || {})
|
||||
|
||||
const recordResults = await Promise.allSettled(
|
||||
['source', 'target'].flatMap(direction => listQueryStatuses.map(status => fetchDirectionRecords(direction, status)))
|
||||
)
|
||||
const successfulGroups = recordResults.filter(result => result.status === 'fulfilled').map(result => result.value)
|
||||
if (!successfulGroups.length) throw new Error('加载迁移任务失败')
|
||||
const allRows = successfulGroups.flat()
|
||||
const uniqueRows = allRows.filter((row, index, rows) => rows.findIndex(item => item.id === row.id) === index)
|
||||
const candidates = uniqueRows.filter(row => activeStatuses.has(row.status))
|
||||
const progressResults = await Promise.allSettled(candidates.map(row => getDataMigrateProgress({ migration_record_id: row.id })))
|
||||
queueRecords.value = candidates.map((row, index) => {
|
||||
const result = progressResults[index]
|
||||
const progress = result.status === 'fulfilled' && result.value?.data?.code === 200 ? result.value.data.data : null
|
||||
return progress ? { ...row, ...progress } : row
|
||||
}).filter(row => activeStatuses.has(row.status)).sort((a, b) => {
|
||||
const aQueued = Number(a.queue_position) > 0
|
||||
const bQueued = Number(b.queue_position) > 0
|
||||
if (aQueued !== bQueued) return aQueued ? 1 : -1
|
||||
if (aQueued) return Number(a.queue_position) - Number(b.queue_position)
|
||||
return new Date(a.started_at || a.CreatedAt || 0) - new Date(b.started_at || b.CreatedAt || 0)
|
||||
})
|
||||
const params = { service_id: serviceId.value, relation: 'any', page: page.value, count: pageSize.value }
|
||||
if (silent && revision.value) params.after_revision = revision.value
|
||||
const res = await getDataMigrateQueue(params)
|
||||
if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '加载迁移队列失败'))
|
||||
const data = res.data.data || {}
|
||||
revision.value = Number(data.revision) || revision.value
|
||||
if (data.changed === false) return
|
||||
queueRecords.value = Array.isArray(data.data) ? data.data : []
|
||||
total.value = Number(data.all_count) || 0
|
||||
Object.assign(concurrency, data.summary || {})
|
||||
} catch (error) {
|
||||
loadError.value = error.message || extractApiError(error?.response?.data, '加载迁移队列失败')
|
||||
if (!silent) ElMessage.error(loadError.value)
|
||||
@@ -233,6 +214,8 @@ const submitConcurrency = async () => {
|
||||
const result = res.data.data || {}
|
||||
ElMessage.success(`并发数已从 ${result.old_concurrency ?? concurrency.concurrency} 调整为 ${result.new_concurrency ?? concurrencyForm.value},放行 ${result.released_count || 0} 项任务`)
|
||||
concurrencyVisible.value = false
|
||||
if (result.summary) Object.assign(concurrency, result.summary)
|
||||
revision.value = 0
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || extractApiError(error?.response?.data, '更新迁移并发失败'))
|
||||
@@ -247,14 +230,15 @@ const abortMigration = async row => {
|
||||
confirmButtonText: '确定中断', cancelButtonText: '取消', type: 'warning'
|
||||
})
|
||||
} catch { return }
|
||||
abortingId.value = row.id
|
||||
abortingId.value = row.migration_record_id
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('service_id', row.source_service_id)
|
||||
formData.append('vm_id', row.source_vm_id)
|
||||
const res = await abortDataMigrate(formData)
|
||||
formData.append('migration_record_id', row.migration_record_id)
|
||||
const res = await abortDataMigrateQueue(formData)
|
||||
if (res?.data?.code !== 200) throw new Error(extractApiError(res?.data, '中断迁移失败'))
|
||||
ElMessage.success('迁移任务已中断')
|
||||
const result = res.data.data || {}
|
||||
ElMessage.success(`迁移任务已中断,释放 ${result.released_count || 0} 个队列任务`)
|
||||
revision.value = 0
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || extractApiError(error?.response?.data, '中断迁移失败'))
|
||||
@@ -269,12 +253,31 @@ const goVm = (targetServiceId, vmId) => router.push({ path: '/virtualization/vm-
|
||||
const goUser = userId => router.push({ path: '/user/detail', query: { user_id: userId } })
|
||||
const goUserGoods = id => router.push(`/user-goods/detail/${id}`)
|
||||
|
||||
const startPolling = () => {
|
||||
if (!refreshTimer) refreshTimer = window.setInterval(() => loadData({ silent: true }), 5000)
|
||||
}
|
||||
const stopPolling = () => {
|
||||
if (refreshTimer) window.clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
const handlePageChange = () => { revision.value = 0; loadData() }
|
||||
const handleSizeChange = () => { page.value = 1; revision.value = 0; loadData() }
|
||||
|
||||
onMounted(async () => {
|
||||
await loadData()
|
||||
refreshTimer = window.setInterval(() => loadData({ silent: true }), 5000)
|
||||
if (!activeDetailTab || activeDetailTab.value === 'migration_queue') {
|
||||
await loadData()
|
||||
startPolling()
|
||||
}
|
||||
})
|
||||
watch(() => activeDetailTab?.value, async tab => {
|
||||
if (tab === 'migration_queue') {
|
||||
revision.value = 0
|
||||
await loadData()
|
||||
startPolling()
|
||||
} else stopPolling()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshTimer) window.clearInterval(refreshTimer)
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -299,6 +302,7 @@ onBeforeUnmount(() => {
|
||||
.stack-cell { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
|
||||
.progress-meta { display: flex; justify-content: space-between; gap: 8px; margin-top: 3px; }
|
||||
.dialog-alert { margin-bottom: 18px; }
|
||||
.queue-pagination { justify-content: flex-end; margin-top: 18px; }
|
||||
.form-hint { margin-left: 10px; color: #909399; font-size: 12px; }
|
||||
@media (max-width: 900px) {
|
||||
.queue-header { flex-direction: column; }
|
||||
|
||||
Reference in New Issue
Block a user