feat(frontend): drive delivery steps by backend stage events
- Map precheck/install/configure/healthcheck/register stage events to workbench step states instead of guess-based progression, and prefer callback-enabled AWX templates so stage events actually flow - Replay task events silently on workbench restore to rebuild step states without duplicating logs; only reset seen events for new tasks - Unify instance name generation with a random 6-char suffix (replacing the hardcoded -billing-02) and cap the business-line segment so the 63-char DNS label limit never truncates the suffix
This commit is contained in:
@@ -818,7 +818,7 @@ const canManageRollback = computed(() => authStore.isAdmin && lastDeliveryStatus
|
|||||||
const canRetryCloudDM = computed(() => deliveryRegisterFailed.value && Boolean(deploymentId.value))
|
const canRetryCloudDM = computed(() => deliveryRegisterFailed.value && Boolean(deploymentId.value))
|
||||||
|
|
||||||
const deliveryForm = reactive({
|
const deliveryForm = reactive({
|
||||||
instanceName: `mysql-${currentName.value}-billing-02`,
|
instanceName: generateInstanceName(activeServiceKey.value, currentName.value),
|
||||||
version: 'MySQL 8.0',
|
version: 'MySQL 8.0',
|
||||||
mode: 'single',
|
mode: 'single',
|
||||||
spec: '1C / 2G',
|
spec: '1C / 2G',
|
||||||
@@ -853,6 +853,13 @@ const collationMap: Record<string, string[]> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const steps = ref<DeliveryStep[]>([])
|
const steps = ref<DeliveryStep[]>([])
|
||||||
|
const deliveryStageStepIndex: Record<string, number> = {
|
||||||
|
precheck: 0,
|
||||||
|
install: 1,
|
||||||
|
configure: 2,
|
||||||
|
healthcheck: 3,
|
||||||
|
register: 4,
|
||||||
|
}
|
||||||
|
|
||||||
const activeService = computed(() => basicServices.value.find((service) => service.key === activeServiceKey.value && !service.disabled))
|
const activeService = computed(() => basicServices.value.find((service) => service.key === activeServiceKey.value && !service.disabled))
|
||||||
const selectedTarget = computed(() => deliveryTargets.value.find((target) => target.id === selectedTargetId.value))
|
const selectedTarget = computed(() => deliveryTargets.value.find((target) => target.id === selectedTargetId.value))
|
||||||
@@ -968,7 +975,7 @@ const deliveryStateClass = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(currentName, (name) => {
|
watch(currentName, (name) => {
|
||||||
deliveryForm.instanceName = `${activeServiceKey.value === 'mysql' ? 'mysql' : 'nginx'}-${name}-billing-02`
|
deliveryForm.instanceName = generateInstanceName(activeServiceKey.value, name)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(selectedTargetId, () => {
|
watch(selectedTargetId, () => {
|
||||||
@@ -1120,8 +1127,11 @@ async function restoreWorkbench() {
|
|||||||
|
|
||||||
restoringWorkbench = true
|
restoringWorkbench = true
|
||||||
Object.assign(deliveryForm, snapshot.form)
|
Object.assign(deliveryForm, snapshot.form)
|
||||||
|
if (!snapshot.deploymentId) {
|
||||||
|
deliveryForm.instanceName = generateInstanceName(activeServiceKey.value, currentName.value)
|
||||||
|
}
|
||||||
deliveryForm.rootPassword = generateRootPassword()
|
deliveryForm.rootPassword = generateRootPassword()
|
||||||
precheckPassed.value = snapshot.precheckPassed
|
precheckPassed.value = snapshot.deploymentId ? snapshot.precheckPassed : false
|
||||||
running.value = snapshot.running
|
running.value = snapshot.running
|
||||||
deliveryDone.value = snapshot.deliveryDone
|
deliveryDone.value = snapshot.deliveryDone
|
||||||
deliveryFailed.value = snapshot.deliveryFailed
|
deliveryFailed.value = snapshot.deliveryFailed
|
||||||
@@ -1145,7 +1155,8 @@ async function restoreWorkbench() {
|
|||||||
lastDeliveryStatus.value = data.task.status
|
lastDeliveryStatus.value = data.task.status
|
||||||
credentialAvailable.value = data.task.credential_available
|
credentialAvailable.value = data.task.credential_available
|
||||||
deliveryError.value = data.task.error_message || deliveryError.value
|
deliveryError.value = data.task.error_message || deliveryError.value
|
||||||
seenEventIds.value = new Set(data.events.map((event) => event.id))
|
seenEventIds.value = new Set()
|
||||||
|
applyTaskEvents(data.events, false)
|
||||||
applyDeliveryStatus(data.task.status, data.task.error_message || '')
|
applyDeliveryStatus(data.task.status, data.task.error_message || '')
|
||||||
if (isTerminalDeliveryStatus(data.task.status)) {
|
if (isTerminalDeliveryStatus(data.task.status)) {
|
||||||
activeView.value = 'result'
|
activeView.value = 'result'
|
||||||
@@ -1173,8 +1184,7 @@ async function restoreWorkbench() {
|
|||||||
function hydrateServiceDefaults() {
|
function hydrateServiceDefaults() {
|
||||||
const service = activeService.value
|
const service = activeService.value
|
||||||
if (!service) return
|
if (!service) return
|
||||||
const prefix = service.key === 'mysql' ? 'mysql' : 'nginx'
|
deliveryForm.instanceName = generateInstanceName(service.key, currentName.value)
|
||||||
deliveryForm.instanceName = `${prefix}-${currentName.value}-billing-02`
|
|
||||||
deliveryForm.version = service.versions[0] || ''
|
deliveryForm.version = service.versions[0] || ''
|
||||||
deliveryForm.mode = service.modes[1]?.value || service.modes[0]?.value || 'single'
|
deliveryForm.mode = service.modes[1]?.value || service.modes[0]?.value || 'single'
|
||||||
deliveryForm.spec = service.key === 'mysql'
|
deliveryForm.spec = service.key === 'mysql'
|
||||||
@@ -1235,6 +1245,7 @@ function resetExecutionState() {
|
|||||||
deliveredHost.value = ''
|
deliveredHost.value = ''
|
||||||
deliveredPort.value = undefined
|
deliveredPort.value = undefined
|
||||||
deliveryLog.value = '[ready] 等待创建交付任务...'
|
deliveryLog.value = '[ready] 等待创建交付任务...'
|
||||||
|
seenEventIds.value = new Set()
|
||||||
steps.value = defaultSteps().map((step) => ({ ...step, state: 'pending' }))
|
steps.value = defaultSteps().map((step) => ({ ...step, state: 'pending' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1357,7 +1368,7 @@ async function createTask() {
|
|||||||
upsertDeliveryHistory(task.id, task.status, task.error_message || '', task.created_at)
|
upsertDeliveryHistory(task.id, task.status, task.error_message || '', task.created_at)
|
||||||
deliveryLog.value += `\n[task] ${task.id} created by ${currentName.value}`
|
deliveryLog.value += `\n[task] ${task.id} created by ${currentName.value}`
|
||||||
applyDeliveryStatus(task.status, '')
|
applyDeliveryStatus(task.status, '')
|
||||||
startTaskPolling(task.id)
|
startTaskPolling(task.id, true)
|
||||||
persistWorkbench()
|
persistWorkbench()
|
||||||
ElMessage.success('交付任务已创建')
|
ElMessage.success('交付任务已创建')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1430,9 +1441,9 @@ function hostLabel(host: TargetHostOption) {
|
|||||||
return host.ip ? `${host.name} · ${host.ip}` : host.name
|
return host.ip ? `${host.name} · ${host.ip}` : host.name
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTaskPolling(id: string) {
|
function startTaskPolling(id: string, resetEvents = false) {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
seenEventIds.value = new Set()
|
if (resetEvents) seenEventIds.value = new Set()
|
||||||
pollTask(id)
|
pollTask(id)
|
||||||
pollTimer.value = window.setInterval(() => pollTask(id), 3000)
|
pollTimer.value = window.setInterval(() => pollTask(id), 3000)
|
||||||
}
|
}
|
||||||
@@ -1461,14 +1472,30 @@ async function pollTask(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTaskEvents(events: TaskEvent[]) {
|
function applyTaskEvents(events: TaskEvent[], writeLogs = true) {
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
if (seenEventIds.value.has(event.id)) return
|
if (seenEventIds.value.has(event.id)) return
|
||||||
seenEventIds.value.add(event.id)
|
seenEventIds.value.add(event.id)
|
||||||
appendLog(`[${event.to_state}] ${event.message}`)
|
applyStageEvent(event)
|
||||||
|
if (writeLogs) appendLog(`[${event.to_state}] ${event.message}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyStageEvent(event: TaskEvent) {
|
||||||
|
const stage = String(event.stage || '').toLowerCase()
|
||||||
|
const eventStatus = String(event.event_status || '').toLowerCase()
|
||||||
|
const stepIndex = deliveryStageStepIndex[stage]
|
||||||
|
if (stepIndex === undefined) return
|
||||||
|
const stateMap: Record<string, StepState> = {
|
||||||
|
running: 'running',
|
||||||
|
success: 'done',
|
||||||
|
failed: 'failed',
|
||||||
|
}
|
||||||
|
const state = stateMap[eventStatus]
|
||||||
|
if (!state) return
|
||||||
|
setDeliveryStep(stepIndex, state)
|
||||||
|
}
|
||||||
|
|
||||||
function appendLog(message: unknown) {
|
function appendLog(message: unknown) {
|
||||||
if (!message) return
|
if (!message) return
|
||||||
deliveryLog.value += `\n${String(message)}`
|
deliveryLog.value += `\n${String(message)}`
|
||||||
@@ -1487,23 +1514,41 @@ async function copyText(value: string, message: string) {
|
|||||||
function applyDeliveryStatus(status: string, message: string) {
|
function applyDeliveryStatus(status: string, message: string) {
|
||||||
lastDeliveryStatus.value = status
|
lastDeliveryStatus.value = status
|
||||||
if (message) appendLog(message)
|
if (message) appendLog(message)
|
||||||
if (['pending', 'validating', 'dispatching', 'running', 'registering', 'canceling', 'rollback_pending', 'rolling_back'].includes(status)) {
|
if (['pending', 'validating', 'dispatching', 'running'].includes(status)) {
|
||||||
running.value = true
|
running.value = true
|
||||||
markStepRunning()
|
if (!steps.value.slice(0, 5).some((step) => step.state !== 'pending')) {
|
||||||
|
setDeliveryStep(0, 'running')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (status === 'registering') {
|
||||||
|
running.value = true
|
||||||
|
setDeliveryStep(4, 'running')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (status === 'canceling') {
|
||||||
|
running.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (['rollback_pending', 'rolling_back'].includes(status)) {
|
||||||
|
running.value = true
|
||||||
|
setDeliveryStep(5, 'running')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (status === 'finished') {
|
if (status === 'finished') {
|
||||||
running.value = false
|
running.value = false
|
||||||
deliveryDone.value = true
|
deliveryDone.value = true
|
||||||
deliveryFailed.value = false
|
deliveryFailed.value = false
|
||||||
steps.value = steps.value.map((step) => ({ ...step, state: 'done' }))
|
steps.value = steps.value.map((step, index) => (
|
||||||
|
index < 5 ? { ...step, state: 'done' } : step
|
||||||
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (status === 'rolled_back') {
|
if (status === 'rolled_back') {
|
||||||
running.value = false
|
running.value = false
|
||||||
deliveryDone.value = false
|
deliveryDone.value = false
|
||||||
deliveryFailed.value = true
|
deliveryFailed.value = true
|
||||||
steps.value = steps.value.map((step) => ({ ...step, state: 'done' }))
|
setDeliveryStep(5, 'done')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (['execution_failed', 'validation_failed', 'register_failed', 'rollback_failed', 'rollback_acknowledged', 'canceled'].includes(status)) {
|
if (['execution_failed', 'validation_failed', 'register_failed', 'rollback_failed', 'rollback_acknowledged', 'canceled'].includes(status)) {
|
||||||
@@ -1511,7 +1556,11 @@ function applyDeliveryStatus(status: string, message: string) {
|
|||||||
deliveryDone.value = false
|
deliveryDone.value = false
|
||||||
deliveryFailed.value = true
|
deliveryFailed.value = true
|
||||||
deliveryError.value = message || deliveryError.value
|
deliveryError.value = message || deliveryError.value
|
||||||
markCurrentStepFailed()
|
if (status === 'validation_failed') setDeliveryStep(0, 'failed')
|
||||||
|
else if (status === 'register_failed') setDeliveryStep(4, 'failed')
|
||||||
|
else if (status === 'rollback_failed') setDeliveryStep(5, 'failed')
|
||||||
|
else if (status === 'rollback_acknowledged') setDeliveryStep(5, 'done')
|
||||||
|
else markCurrentStepFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1519,7 +1568,8 @@ async function loadDeliveryTargets() {
|
|||||||
targetsLoading.value = true
|
targetsLoading.value = true
|
||||||
try {
|
try {
|
||||||
deliveryTargets.value = await deliveryApi.listTargets()
|
deliveryTargets.value = await deliveryApi.listTargets()
|
||||||
const preferred = deliveryTargets.value.find((target) => !/callback|rollback/i.test(target.name))
|
const preferred = deliveryTargets.value.find((target) => /callback/i.test(target.name))
|
||||||
|
|| deliveryTargets.value.find((target) => !/rollback/i.test(target.name))
|
||||||
selectedTargetId.value = preferred?.id || deliveryTargets.value[0]?.id
|
selectedTargetId.value = preferred?.id || deliveryTargets.value[0]?.id
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '获取部署目标失败')
|
ElMessage.error(error instanceof Error ? error.message : '获取部署目标失败')
|
||||||
@@ -1623,6 +1673,17 @@ function normalizeDNSLabel(value: string) {
|
|||||||
return normalized || 'default'
|
return normalized || 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateInstanceName(serviceKey: string, businessLineName: string) {
|
||||||
|
const prefix = serviceKey === 'mysql' ? 'mysql' : 'nginx'
|
||||||
|
// 限长业务线段,确保 63 字符截断不会吃掉随机后缀(prefix 5 + 连字符 2 + 后缀 6 = 13)
|
||||||
|
const businessLine = normalizeDNSLabel(businessLineName).slice(0, 50).replace(/-+$/g, '')
|
||||||
|
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
const values = new Uint32Array(6)
|
||||||
|
crypto.getRandomValues(values)
|
||||||
|
const suffix = Array.from(values, (value) => alphabet[value % alphabet.length]).join('')
|
||||||
|
return normalizeDNSLabel(`${prefix}-${businessLine}-${suffix}`)
|
||||||
|
}
|
||||||
|
|
||||||
function generateRootPassword() {
|
function generateRootPassword() {
|
||||||
const groups = [
|
const groups = [
|
||||||
'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
||||||
@@ -1666,13 +1727,7 @@ function regenerateRootPassword() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function regenerateInstanceName() {
|
function regenerateInstanceName() {
|
||||||
const prefix = activeServiceKey.value === 'mysql' ? 'mysql' : 'service'
|
deliveryForm.instanceName = generateInstanceName(activeServiceKey.value, currentName.value)
|
||||||
const businessLine = normalizeDNSLabel(currentName.value)
|
|
||||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
||||||
const values = new Uint32Array(6)
|
|
||||||
crypto.getRandomValues(values)
|
|
||||||
const suffix = Array.from(values, (value) => alphabet[value % alphabet.length]).join('')
|
|
||||||
deliveryForm.instanceName = normalizeDNSLabel(`${prefix}-${businessLine}-${suffix}`)
|
|
||||||
precheckPassed.value = false
|
precheckPassed.value = false
|
||||||
ElMessage.success('已生成新的实例名称,请执行预检查确认可用')
|
ElMessage.success('已生成新的实例名称,请执行预检查确认可用')
|
||||||
}
|
}
|
||||||
@@ -1934,12 +1989,11 @@ async function releaseRollback() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function markStepRunning() {
|
function setDeliveryStep(index: number, state: StepState) {
|
||||||
const index = steps.value.findIndex((step) => step.state === 'pending' || step.state === 'running')
|
if (index < 0 || index >= steps.value.length) return
|
||||||
if (index < 0) return
|
|
||||||
steps.value = steps.value.map((step, stepIndex) => {
|
steps.value = steps.value.map((step, stepIndex) => {
|
||||||
if (stepIndex < index) return { ...step, state: 'done' }
|
if (stepIndex < index && step.state !== 'failed') return { ...step, state: 'done' }
|
||||||
if (stepIndex === index) return { ...step, state: 'running' }
|
if (stepIndex === index) return { ...step, state }
|
||||||
return step
|
return step
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user