feat: add AWS admin suspension and fix recycle restore network parameter
This commit is contained in:
@@ -25,6 +25,11 @@ export const deleteAwsGoods = (id) => http2.delete(`${base}/goods/delete`, { par
|
||||
export const getAwsInstanceDetail = (identifier) => http2.get(`${base}/instances/detail`, {
|
||||
params: typeof identifier === 'object' ? identifier : { instance_id: identifier }
|
||||
})
|
||||
export const actionAdminAwsInstance = (instanceId, action, reason) => http2.post(`${base}/instances/action`, {
|
||||
instance_id: instanceId,
|
||||
action,
|
||||
...(action === 'suspend' ? { reason } : {})
|
||||
})
|
||||
export const retryAwsInstance = (instanceId, note = '') => http2.post(`${base}/instances/retry`, { instance_id: instanceId, note })
|
||||
export const resyncAwsInstance = (instanceId, note = '') => http2.post(`${base}/instances/resync`, { instance_id: instanceId, note })
|
||||
export const resolveAwsInstance = (instanceId, resolution, note = '') => http2.post(`${base}/instances/resolve`, { instance_id: instanceId, resolution, note })
|
||||
|
||||
@@ -12,6 +12,23 @@
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="instance" class="toolbar">
|
||||
<span>管理员控制</span>
|
||||
<div>
|
||||
<el-button type="warning" :disabled="lifecycleSubmitting" @click="openSuspend">管理员暂停</el-button>
|
||||
<el-button type="success" plain :disabled="lifecycleSubmitting" @click="resumeInstance">解除暂停</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="suspendDialog" title="管理员暂停实例" width="500px" :close-on-click-modal="false" :close-on-press-escape="!lifecycleSubmitting" :show-close="!lifecycleSubmitting">
|
||||
<el-alert title="暂停后用户将无法操作此实例,只有管理员解除暂停后才能恢复操作。" type="warning" show-icon :closable="false" class="alert"/>
|
||||
<el-form ref="suspendFormRef" :model="suspendForm" label-width="90px">
|
||||
<el-form-item label="暂停原因" prop="reason" :rules="[{ required: true, whitespace: true, message: '请输入暂停原因', trigger: 'blur' }]">
|
||||
<el-input v-model="suspendForm.reason" type="textarea" :rows="4" :disabled="lifecycleSubmitting" placeholder="请输入暂停原因"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button :disabled="lifecycleSubmitting" @click="suspendDialog=false">取消</el-button><el-button type="warning" :loading="lifecycleSubmitting" @click="submitSuspend">确认暂停</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-tabs v-if="instance" v-model="tab" class="tabs" @tab-change="tabChanged">
|
||||
<el-tab-pane label="履约审核" name="fulfillment">
|
||||
<div class="toolbar"><span>履约记录与失败审核</span><div><el-button @click="fulfillmentAction('resync')">重新同步</el-button><el-button v-if="instance.provisionStatus==='failed'" type="warning" @click="fulfillmentAction('retry')">审核重试</el-button><el-button v-if="instance.provisionStatus==='failed'" type="success" @click="fulfillmentAction('resolved')">确认解决</el-button><el-button v-if="instance.provisionStatus==='failed'" type="danger" plain @click="fulfillmentAction('reject')">驳回</el-button></div></div>
|
||||
@@ -85,6 +102,8 @@ const protocols=['tcp','udp','icmp','icmpv6','-1'],volumeTypes=['gp3','gp2','io1
|
||||
const rule=reactive({direction:'ingress',protocol:'tcp',from_port:22,to_port:22,cidr:'0.0.0.0/0',description:''})
|
||||
const volumeForm=reactive({volume_id:'',name:'',device_name:'/dev/sdf',size_gib:20,type:'gp3',iops:0,throughput:0,encrypted:true,kms_key_id:'',delete_on_termination:false})
|
||||
const trafficForm=reactive({enabled:true,limit_gib:100})
|
||||
const suspendDialog=ref(false),suspendFormRef=ref(null),lifecycleSubmitting=ref(false)
|
||||
const suspendForm=reactive({reason:''})
|
||||
const envelope=r=>r?.data||r||{},dataOf=r=>envelope(r)?.data,message=(e,f)=>e?.response?.data?.message||e?.response?.data?.msg||envelope(e)?.message||e?.message||f
|
||||
const instance=computed(()=>baseDetail.value?.instance||null)
|
||||
const securityRules=computed(()=>{const d=security.value||{};if(Array.isArray(d.rules))return d.rules;return[...(d.ingress_rules||d.ingressRules||[]).map(v=>({...v,direction:v.direction||'ingress'})),...(d.egress_rules||d.egressRules||[]).map(v=>({...v,direction:v.direction||'egress'}))]})
|
||||
@@ -119,6 +138,28 @@ async function loadVolumes(){const v=await safe(()=>api.getAdminAwsVolumes(insta
|
||||
async function loadOperations(){const v=await safe(()=>api.getAdminAwsOperations(instanceId,{page:operationPage.value,count:20}),'读取操作记录失败');if(v){operations.value=v.list||[];operationTotal.value=v.all_count||0}}
|
||||
function tabChanged(name){if(name==='runtime'){if(!runtimeDetails.value)loadRuntime();if(!monitoring.value)loadMonitoring();if(!trafficLimit.value)loadTraffic()}if(name==='network'&&!security.value)loadNetwork();if(name==='storage'&&!volumes.value.length)loadVolumes();if(name==='operations')loadOperations()}
|
||||
async function promptNote(title){try{return(await ElMessageBox.prompt('请输入操作备注(可留空)',title,{inputValue:''})).value}catch{return null}}
|
||||
function openSuspend(){suspendForm.reason='';suspendFormRef.value?.clearValidate();suspendDialog.value=true}
|
||||
async function submitSuspend(){
|
||||
if(lifecycleSubmitting.value)return
|
||||
if(!await suspendFormRef.value?.validate().catch(()=>false))return
|
||||
await submitLifecycleAction('suspend',suspendForm.reason.trim())
|
||||
}
|
||||
async function resumeInstance(){
|
||||
if(lifecycleSubmitting.value)return
|
||||
try{await ElMessageBox.confirm(`确认解除实例 #${instanceId} 的管理员暂停?解除后用户可恢复操作。`,'解除暂停',{type:'warning',confirmButtonText:'确认解除',cancelButtonText:'取消'})}catch{return}
|
||||
await submitLifecycleAction('resume')
|
||||
}
|
||||
async function submitLifecycleAction(action,reason){
|
||||
if(lifecycleSubmitting.value)return
|
||||
lifecycleSubmitting.value=true
|
||||
try{
|
||||
const result=envelope(await api.actionAdminAwsInstance(instanceId,action,reason))
|
||||
if(result.code!==200)throw new Error(result.message||'实例操作失败')
|
||||
suspendDialog.value=false
|
||||
ElMessage.success(action==='suspend'?'管理员暂停已受理':'解除暂停已受理')
|
||||
await Promise.all([loadBase(),loadRuntime()])
|
||||
}catch(e){ElMessage.error(message(e,'实例操作失败'))}finally{lifecycleSubmitting.value=false}
|
||||
}
|
||||
async function fulfillmentAction(type){const note=await promptNote({resync:'重新同步履约',retry:'审核重试',resolved:'确认解决',reject:'驳回履约'}[type]);if(note===null)return;let call;if(type==='resync')call=()=>api.resyncAwsInstance(instanceId,note);else if(type==='retry')call=()=>api.retryAwsInstance(instanceId,note);else call=()=>api.resolveAwsInstance(instanceId,type,note);const v=await safe(call,'履约操作失败');if(v!==null){ElMessage.success('操作成功');loadBase()}}
|
||||
function stopPoll(){clearTimeout(pollTimer.value);pollTimer.value=null}
|
||||
function pollOperation(id,onSuccess){stopPoll();pollStarted.value=Date.now();const tick=async()=>{if(Date.now()-pollStarted.value>=120000)return ElMessage.warning('操作仍在处理,请稍后手动刷新');try{const r=await api.getAdminAwsOperationDetail(id);const op=dataOf(r);if(['queued','running'].includes(op?.status)){pollTimer.value=setTimeout(tick,5000);return}if(op?.status==='succeeded'){ElMessage.success('操作完成');await onSuccess?.()}else ElMessage.error(op?.errorMessage||'操作失败')}catch{pollTimer.value=setTimeout(tick,5000)}};pollTimer.value=setTimeout(tick,5000)}
|
||||
|
||||
@@ -304,7 +304,7 @@ const submitRestore = async () => {
|
||||
selectedNatNetworks.value.forEach(n => fd.append('network_ids', n.id))
|
||||
}
|
||||
if (selectedBridgeNetwork.value) {
|
||||
fd.append('internet_network_id', selectedBridgeNetwork.value.id)
|
||||
fd.append('network_ids', selectedBridgeNetwork.value.id)
|
||||
}
|
||||
const res = await restoreRecycleBin(fd)
|
||||
if (res?.data?.code === 200) {
|
||||
|
||||
Reference in New Issue
Block a user