251 lines
9.2 KiB
Vue
251 lines
9.2 KiB
Vue
<script setup lang="ts">
|
||
import { NButton, NPopconfirm, NSpin } from 'naive-ui'
|
||
|
||
import { computed, ref } from 'vue'
|
||
|
||
import { addVnicIpv6, detachVnic, listVnics, removeIpv6 } from '@/api/instances'
|
||
import FootNote from '@/components/FootNote.vue'
|
||
import AttachVnicModal from '@/components/instance/AttachVnicModal.vue'
|
||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||
import OcidText from '@/components/OcidText.vue'
|
||
import { useAsync } from '@/composables/useAsync'
|
||
import { ATTACHMENT_TRANSIENT_STATES, useTransientPoll } from '@/composables/useTransientPoll'
|
||
import { useToast } from '@/composables/useToast'
|
||
|
||
/** 网卡(VNIC)区块:每卡一主行 + IPv6 副行;IPv6 增删按卡操作。
|
||
* 删除复用实例级接口(后端按地址遍历全部网卡),添加走 per-VNIC 接口。 */
|
||
const props = defineProps<{
|
||
cfgId: number
|
||
instanceId: string
|
||
region?: string
|
||
/** 实例已终止 / 终止中:全部变更操作不可用 */
|
||
disabled?: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{ changed: [] }>()
|
||
|
||
const message = useToast()
|
||
const vnics = useAsync(() => listVnics(props.cfgId, props.instanceId, props.region))
|
||
|
||
/** 附加已受理但 OCI 挂载列表有可见性延迟:限时轮询等新卡入列,入列后交由过渡态轮询接管 */
|
||
const expectAtLeast = ref(0)
|
||
let expectUntil = 0
|
||
|
||
useTransientPoll(
|
||
() => vnics.data.value,
|
||
(list) =>
|
||
!!list?.some((v) => ATTACHMENT_TRANSIENT_STATES.includes(v.lifecycleState)) ||
|
||
((list?.length ?? 0) < expectAtLeast.value && Date.now() < expectUntil),
|
||
() => void vnics.run({ silent: true }),
|
||
)
|
||
|
||
const showAttach = ref(false)
|
||
const detaching = ref('')
|
||
const addingIpv6 = ref('')
|
||
const removingIpv6 = ref('')
|
||
const busy = computed(() => !!detaching.value || !!addingIpv6.value || !!removingIpv6.value)
|
||
|
||
function refresh() {
|
||
void vnics.run()
|
||
emit('changed')
|
||
}
|
||
|
||
function onAttached() {
|
||
expectAtLeast.value = (vnics.data.value ?? []).length + 1
|
||
expectUntil = Date.now() + 90_000
|
||
refresh()
|
||
}
|
||
|
||
async function doDetach(attachmentId: string) {
|
||
detaching.value = attachmentId
|
||
try {
|
||
await detachVnic(props.cfgId, attachmentId, props.region)
|
||
message.success('分离请求已提交')
|
||
refresh()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '分离失败')
|
||
} finally {
|
||
detaching.value = ''
|
||
}
|
||
}
|
||
|
||
async function doAddIpv6(vnicId: string) {
|
||
addingIpv6.value = vnicId
|
||
try {
|
||
await addVnicIpv6(props.cfgId, vnicId, '', props.region)
|
||
message.success('IPv6 已添加')
|
||
refresh()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '添加失败,请确认子网已启用 IPv6')
|
||
} finally {
|
||
addingIpv6.value = ''
|
||
}
|
||
}
|
||
|
||
async function doRemoveIpv6(addr: string) {
|
||
removingIpv6.value = addr
|
||
try {
|
||
await removeIpv6(props.cfgId, props.instanceId, addr, props.region)
|
||
message.success('IPv6 已取消分配')
|
||
refresh()
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败')
|
||
} finally {
|
||
removingIpv6.value = ''
|
||
}
|
||
}
|
||
|
||
defineExpose({ refresh: () => vnics.run() })
|
||
</script>
|
||
|
||
<template>
|
||
<div class="panel">
|
||
<div class="flex items-center justify-between gap-3 border-b border-line-soft px-4.5 py-3.5">
|
||
<div class="min-w-0">
|
||
<div class="text-sm font-semibold">网卡(VNIC)</div>
|
||
<div class="mt-0.5 text-xs text-ink-3">
|
||
实例的全部虚拟网卡;每张网卡可各自附加多个 IPv6
|
||
</div>
|
||
</div>
|
||
<NButton size="small" type="primary" :disabled="disabled" @click="showAttach = true">
|
||
附加 VNIC
|
||
</NButton>
|
||
</div>
|
||
|
||
<div
|
||
v-if="vnics.loading.value && !vnics.data.value"
|
||
class="flex items-center justify-center py-8"
|
||
>
|
||
<NSpin size="small" />
|
||
</div>
|
||
<div v-else class="overflow-x-auto">
|
||
<table class="w-full min-w-[760px] border-collapse text-[13px]">
|
||
<thead>
|
||
<tr class="text-left text-xs text-ink-3">
|
||
<th class="w-[26%] border-b border-line-soft py-2 pr-3 pl-4.5 font-medium">名称</th>
|
||
<th class="border-b border-line-soft px-3 py-2 font-medium">私网 IP</th>
|
||
<th class="border-b border-line-soft px-3 py-2 font-medium">公网 IP</th>
|
||
<th class="border-b border-line-soft px-3 py-2 font-medium">子网</th>
|
||
<th class="w-28 border-b border-line-soft px-3 py-2 font-medium">状态</th>
|
||
<th class="w-18 border-b border-line-soft py-2 pr-4.5 pl-3 font-medium">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-for="v in vnics.data.value ?? []" :key="v.attachmentId">
|
||
<tr>
|
||
<td class="py-2.5 pr-3 pb-1.5 pl-4.5">
|
||
<div class="flex min-w-0 items-center gap-2">
|
||
<span class="truncate font-medium" :title="v.displayName">
|
||
{{ v.displayName || '(未命名)' }}
|
||
</span>
|
||
<span
|
||
v-if="v.isPrimary"
|
||
class="flex-none rounded bg-accent/10 px-1.5 py-px text-[10.5px] font-semibold text-accent"
|
||
>
|
||
主网卡
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td class="mono px-3 py-2.5 pb-1.5">{{ v.privateIp || '—' }}</td>
|
||
<td class="mono px-3 py-2.5 pb-1.5">{{ v.publicIp || '—' }}</td>
|
||
<td class="px-3 py-2.5 pb-1.5">
|
||
<span v-if="v.subnetName" class="truncate" :title="v.subnetName">
|
||
{{ v.subnetName }}
|
||
</span>
|
||
<OcidText v-else-if="v.subnetId" :ocid="v.subnetId" />
|
||
<span v-else>—</span>
|
||
</td>
|
||
<td class="px-3 py-2.5 pb-1.5"><LifecycleBadge :state="v.lifecycleState" /></td>
|
||
<td class="py-2.5 pr-4.5 pb-1.5 pl-3">
|
||
<NButton
|
||
v-if="v.isPrimary"
|
||
size="tiny"
|
||
quaternary
|
||
disabled
|
||
title="主网卡不可分离"
|
||
>
|
||
分离
|
||
</NButton>
|
||
<NPopconfirm v-else @positive-click="doDetach(v.attachmentId)">
|
||
<template #trigger>
|
||
<NButton
|
||
size="tiny"
|
||
quaternary
|
||
type="error"
|
||
:loading="detaching === v.attachmentId"
|
||
:disabled="disabled || v.lifecycleState !== 'ATTACHED' || busy"
|
||
>
|
||
分离
|
||
</NButton>
|
||
</template>
|
||
分离该网卡?其上全部 IP(含 IPv6)将一并释放
|
||
</NPopconfirm>
|
||
</td>
|
||
</tr>
|
||
<tr class="border-b border-line-soft last:border-b-0">
|
||
<td colspan="6" class="px-4.5 pt-0 pb-2.5">
|
||
<div class="flex flex-wrap items-center gap-1.5">
|
||
<span class="text-[10.5px] font-medium tracking-wider text-ink-3">IPV6</span>
|
||
<template v-if="v.vnicId && v.lifecycleState === 'ATTACHED'">
|
||
<span
|
||
v-for="addr in v.ipv6Addresses ?? []"
|
||
:key="addr"
|
||
class="mono inline-flex max-w-full items-center gap-1 rounded border border-line bg-white px-1.5 py-0.5 text-xs"
|
||
>
|
||
<span class="truncate">{{ addr }}</span>
|
||
<button
|
||
class="flex-none cursor-pointer text-ink-3 hover:text-err disabled:cursor-wait"
|
||
:aria-label="`删除 ${addr}`"
|
||
:disabled="disabled || busy"
|
||
@click="doRemoveIpv6(addr)"
|
||
>
|
||
{{ removingIpv6 === addr ? '…' : '×' }}
|
||
</button>
|
||
</span>
|
||
<span v-if="!(v.ipv6Addresses ?? []).length" class="text-xs text-ink-3">
|
||
无
|
||
</span>
|
||
<NButton
|
||
size="tiny"
|
||
quaternary
|
||
type="primary"
|
||
:loading="addingIpv6 === v.vnicId"
|
||
:disabled="disabled || busy"
|
||
@click="doAddIpv6(v.vnicId)"
|
||
>
|
||
+ IPv6
|
||
</NButton>
|
||
</template>
|
||
<span v-else class="text-xs text-ink-3">
|
||
{{ v.vnicId ? '网卡未在挂载状态,不可操作 IPv6' : '无 · 附加完成后可添加' }}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
<div
|
||
v-if="!(vnics.data.value ?? []).length && !vnics.loading.value"
|
||
class="py-6 text-center text-[13px] text-ink-3"
|
||
>
|
||
{{ vnics.error.value || '无网卡记录' }}
|
||
</div>
|
||
</div>
|
||
|
||
<FootNote>
|
||
附加须实例运行中且 shape 有空余 VNIC 配额 · 主网卡不可分离 · 每卡 IPv6
|
||
可多个(要求所在子网已启用 IPv6)
|
||
</FootNote>
|
||
|
||
<AttachVnicModal
|
||
v-model:show="showAttach"
|
||
:cfg-id="cfgId"
|
||
:instance-id="instanceId"
|
||
:region="region"
|
||
:attached-count="(vnics.data.value ?? []).length"
|
||
@attached="onAttached"
|
||
/>
|
||
</div>
|
||
</template>
|