Files
oci-portal-dash/src/components/instance/VnicPanel.vue
T

251 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>