Files
xinfra/frontend/src/api/delivery.ts
T
Hungerdream f01398cc67 feat(ui): rework MySQL delivery workbench form and flow
Rebuild the MySQL standardized delivery form in Catalog.vue: sectioned layout (scope/resources/db config/advanced params), client-generated root password with regenerate/copy, storage slider, install/data path hints, inline field validation, delivery history and task polling.

Align CreateMySQLDeliveryPayload with the backend MySQLDeliveryInput contract (cpu_milli/memory_mi/storage_gi required, legacy fields optional, drop nonexistent param_template) and send mysql_root_password/mysql_admin_password as a pair so the credential shown to the user is the one actually deployed.
2026-07-28 19:15:14 +08:00

232 lines
6.3 KiB
TypeScript

import { getToken } from '@/utils/auth'
export interface DeliveryTarget {
id: number
name: string
target_type: string
awx_inventory_id: number
awx_template_id: number
enabled: boolean
metadata?: string
}
export interface DeliveryMountPath {
path: string
available_gi: number
fstype?: string
}
export interface CreateMySQLDeliveryPayload {
business_line_id: number
target_id: number
namespace: string
instance_name: string
mysql_version: string
topology: string
mysql_port?: number
data_disk: string
target_host?: string
cpu_milli: number
memory_mi: number
storage_gi: number
cpu_cores?: number
memory_gb?: number
storage_gb?: number
timezone?: string
lower_case_table_names?: number
character_set?: string
collation?: string
max_connections?: string | number
innodb_redo_log_capacity?: string
innodb_flush_log_at_trx_commit?: number
sync_binlog?: number
innodb_io_capacity?: number
long_query_time?: number
binlog_expire_logs_seconds?: number
max_binlog_size?: string
mysql_root_password?: string
mysql_admin_password?: string
}
export interface DeliveryTask {
id: string
business_line_id: number
requested_by: number
component: string
target_type: string
target_id: number
namespace: string
instance_name: string
target_host?: string
target_host_ip?: string
mysql_port?: number
status: string
error_message?: string
created_at: string
updated_at: string
started_at?: string
finished_at?: string
}
export interface TaskEvent {
id: number
task_id: string
from_state: string
to_state: string
stage?: string
event_status?: string
message: string
created_at: string
}
export interface DeliveryTaskSnapshot {
task: DeliveryTask
events: TaskEvent[]
}
export interface DeploymentCredential {
username: string
host: string
password: string
}
export interface MySQLServiceLedgerItem {
name: string
datacenter: string
business_tag: string
instances: number
healthy: number
address: string
status: string
status_class: string
version: string
namespace: string
}
export const deliveryApi = {
async listTargets(): Promise<DeliveryTarget[]> {
const data = await authRequest('/auth/api/v1/delivery/targets?component=mysql')
return Array.isArray(data.items) ? data.items : []
},
async listTargetMountPaths(targetId: number, host: string): Promise<DeliveryMountPath[]> {
const data = await authRequest(`/auth/api/v1/delivery/targets/${targetId}/hosts/${encodeURIComponent(host)}/mount-paths`)
return Array.isArray(data.items) ? data.items : []
},
async createMySQL(payload: CreateMySQLDeliveryPayload): Promise<DeliveryTask> {
const data = await authRequest('/auth/api/v1/delivery/mysql', {
method: 'POST',
headers: {
'Idempotency-Key': createIdempotencyKey(payload),
},
body: JSON.stringify(payload),
})
return data.task
},
async listTasks(params: { businessLineId?: number; component?: string; active?: boolean } = {}): Promise<DeliveryTask[]> {
const search = new URLSearchParams()
if (params.businessLineId) search.set('business_line_id', String(params.businessLineId))
if (params.component) search.set('component', params.component)
if (params.active) search.set('active', 'true')
const query = search.toString()
const data = await authRequest(`/auth/api/v1/delivery/tasks${query ? `?${query}` : ''}`)
return Array.isArray(data.items) ? data.items : []
},
async listMySQLServices(businessLineId: number): Promise<MySQLServiceLedgerItem[]> {
const data = await authRequest(`/auth/api/v1/delivery/business-lines/${businessLineId}/mysql-services`)
return Array.isArray(data.items) ? data.items : []
},
async getTask(taskId: string): Promise<{ task: DeliveryTask; events: TaskEvent[] }> {
const data = await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}`)
return {
task: data.task,
events: Array.isArray(data.events) ? data.events : [],
}
},
async cancel(taskId: string): Promise<void> {
await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/cancel`, {
method: 'POST',
})
},
streamTask(taskId: string): EventSource {
const token = getToken()
const search = new URLSearchParams()
if (token) search.set('access_token', token)
const query = search.toString()
return new EventSource(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/stream${query ? `?${query}` : ''}`)
},
async retryRollback(taskId: string): Promise<void> {
await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/rollback/retry`, {
method: 'POST',
})
},
async releaseRollback(taskId: string): Promise<void> {
await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/rollback/release`, {
method: 'POST',
})
},
async retryCloudDMRegistration(taskId: string): Promise<void> {
await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/clouddm/retry`, {
method: 'POST',
})
},
async revealCredentials(taskId: string): Promise<DeploymentCredential[]> {
const data = await authRequest(`/auth/api/v1/delivery/tasks/${encodeURIComponent(taskId)}/credentials/reveal`, {
method: 'POST',
})
return Array.isArray(data.items) ? data.items : []
},
}
function createIdempotencyKey(payload: CreateMySQLDeliveryPayload) {
return [
'mysql',
payload.business_line_id,
payload.target_id,
payload.namespace,
payload.instance_name,
Date.now(),
].join(':')
}
async function authRequest(path: string, init: RequestInit = {}) {
const token = getToken()
const response = await fetch(path, {
...init,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...init.headers,
},
})
const text = await response.text()
const data = parseResponseBody(text)
if (!response.ok) {
const message = data?.error || data?.message || text || `HTTP ${response.status}`
throw new Error(message)
}
return data || {}
}
function parseResponseBody(text: string) {
if (!text.trim()) {
return {}
}
try {
return JSON.parse(text)
} catch {
return { error: text }
}
}