初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,766 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NDropdown,
|
||||
NPopconfirm,
|
||||
NSpin,
|
||||
useDialog,
|
||||
useMessage,
|
||||
type DataTableColumns,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { getBootVolume } from '@/api/bootVolumes'
|
||||
import { getConfig } from '@/api/configs'
|
||||
import {
|
||||
changePublicIp,
|
||||
deleteConsoleConnection,
|
||||
detachBootVolume,
|
||||
detachVolume,
|
||||
getImage,
|
||||
getInstance,
|
||||
instanceAction,
|
||||
listBootVolumeAttachments,
|
||||
listConsoleConnections,
|
||||
listVolumeAttachments,
|
||||
terminateInstance,
|
||||
updateInstance,
|
||||
} from '@/api/instances'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import AttachVolumeModal from '@/components/instance/AttachVolumeModal.vue'
|
||||
import ConsoleConnModal from '@/components/instance/ConsoleConnModal.vue'
|
||||
import EditBootVolumeModal from '@/components/instance/EditBootVolumeModal.vue'
|
||||
import InstanceTrafficPanel from '@/components/instance/InstanceTrafficPanel.vue'
|
||||
import ReplaceBootVolumeModal from '@/components/instance/ReplaceBootVolumeModal.vue'
|
||||
import ResizeModal from '@/components/instance/ResizeModal.vue'
|
||||
import VnicPanel from '@/components/instance/VnicPanel.vue'
|
||||
import { useConsoleStore } from '@/stores/console'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import OcidText from '@/components/OcidText.vue'
|
||||
import RenameModal from '@/components/RenameModal.vue'
|
||||
import { fmtTime, shortAd, truncOcid, vpusLabel } from '@/composables/useFormat'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import {
|
||||
ATTACHMENT_TRANSIENT_STATES,
|
||||
INSTANCE_TRANSIENT_STATES,
|
||||
useTransientPoll,
|
||||
} from '@/composables/useTransientPoll'
|
||||
import type { BootVolume, ConsoleConnection, PowerAction, VolumeAttachment } from '@/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const { alias: regionAlias } = useRegionAlias()
|
||||
|
||||
const cfgId = computed(() => Number(route.params.cfgId))
|
||||
const instanceId = computed(() => String(route.params.instanceId))
|
||||
/** 实例所在区域:列表页跳转时经 query 传入,缺失时回退实例对象上的短名(后端已归一化) */
|
||||
const region = computed(
|
||||
() => (route.query.region as string) || instance.data.value?.region || undefined,
|
||||
)
|
||||
|
||||
const config = useAsync(() => getConfig(cfgId.value))
|
||||
const instance = useAsync(() =>
|
||||
getInstance(cfgId.value, instanceId.value, (route.query.region as string) || undefined),
|
||||
)
|
||||
const bvAtts = useAsync(() =>
|
||||
listBootVolumeAttachments(cfgId.value, instanceId.value, region.value),
|
||||
)
|
||||
const volAtts = useAsync(() => listVolumeAttachments(cfgId.value, instanceId.value, region.value))
|
||||
const consoles = useAsync(() =>
|
||||
listConsoleConnections(cfgId.value, instanceId.value, region.value),
|
||||
)
|
||||
|
||||
/** 按 imageId 补镜像名称,查询失败时回退 OCID 展示 */
|
||||
const image = useAsync(
|
||||
() => {
|
||||
const inst = instance.data.value
|
||||
if (!inst?.imageId) return Promise.resolve(null)
|
||||
return getImage(cfgId.value, inst.imageId, inst.region).catch(() => null)
|
||||
},
|
||||
false,
|
||||
)
|
||||
watch(
|
||||
() => instance.data.value?.imageId,
|
||||
(v) => {
|
||||
if (v) void image.run()
|
||||
},
|
||||
)
|
||||
|
||||
/** 引导卷挂载补齐卷详情(名称 / 大小 / VPU),支持就地调整 */
|
||||
const bvDetails = useAsync<BootVolume[]>(async () => {
|
||||
const atts = bvAtts.data.value ?? []
|
||||
const list = await Promise.all(
|
||||
atts.map((a) => getBootVolume(cfgId.value, a.bootVolumeId, region.value).catch(() => null)),
|
||||
)
|
||||
return list.filter((b): b is BootVolume => b !== null)
|
||||
}, false)
|
||||
watch(
|
||||
() => bvAtts.data.value,
|
||||
() => void bvDetails.run(),
|
||||
)
|
||||
|
||||
const bvById = computed(() => new Map((bvDetails.data.value ?? []).map((b) => [b.id, b])))
|
||||
|
||||
// 电源 / 挂载操作提交后 OCI 状态异步迁移:过渡态期间每 5 秒静默刷新直到稳态
|
||||
useTransientPoll(
|
||||
() => instance.data.value,
|
||||
(v) => !!v && INSTANCE_TRANSIENT_STATES.includes(v.lifecycleState),
|
||||
() => void instance.run({ silent: true }),
|
||||
)
|
||||
useTransientPoll(
|
||||
() => bvAtts.data.value,
|
||||
(list) => !!list?.some((a) => ATTACHMENT_TRANSIENT_STATES.includes(a.lifecycleState)),
|
||||
() => void bvAtts.run({ silent: true }),
|
||||
)
|
||||
useTransientPoll(
|
||||
() => volAtts.data.value,
|
||||
(list) => !!list?.some((a) => ATTACHMENT_TRANSIENT_STATES.includes(a.lifecycleState)),
|
||||
() => void volAtts.run({ silent: true }),
|
||||
)
|
||||
useTransientPoll(
|
||||
() => consoles.data.value,
|
||||
(list) => !!list?.some((c) => ['CREATING', 'DELETING'].includes(c.lifecycleState)),
|
||||
() => void consoles.run({ silent: true }),
|
||||
)
|
||||
|
||||
const isStopped = computed(() => instance.data.value?.lifecycleState === 'STOPPED')
|
||||
/** 终止 / 终止中:一切变更类操作不可用 */
|
||||
const isEnded = computed(() =>
|
||||
['TERMINATED', 'TERMINATING'].includes(instance.data.value?.lifecycleState ?? ''),
|
||||
)
|
||||
const attachedVolumeIds = computed(() => (volAtts.data.value ?? []).map((a) => a.volumeId))
|
||||
|
||||
/** 创建时若选了随机密码,密码写在 TAG RootPassword 中,这里回显 */
|
||||
const rootPassword = computed(() => instance.data.value?.freeformTags?.RootPassword ?? '')
|
||||
|
||||
async function copyRootPassword() {
|
||||
await navigator.clipboard.writeText(rootPassword.value)
|
||||
message.success('已复制 ROOT 密码')
|
||||
}
|
||||
|
||||
async function copyImageOcid() {
|
||||
await navigator.clipboard.writeText(instance.data.value?.imageId ?? '')
|
||||
message.success('已复制镜像 OCID')
|
||||
}
|
||||
|
||||
const showRename = ref(false)
|
||||
const renaming = ref(false)
|
||||
const showResize = ref(false)
|
||||
const showConsole = ref(false)
|
||||
const showReplaceBv = ref(false)
|
||||
const showAttachVol = ref(false)
|
||||
const showEditBv = ref(false)
|
||||
const editingBv = ref<BootVolume | null>(null)
|
||||
|
||||
function openEditBv(bv: BootVolume) {
|
||||
editingBv.value = bv
|
||||
showEditBv.value = true
|
||||
}
|
||||
|
||||
const consoleStore = useConsoleStore()
|
||||
|
||||
/** VNC 与串行共用实例唯一的控制台连接资源(创建时互相清理),同时只开一个 */
|
||||
function openVnc() {
|
||||
consoleStore.close()
|
||||
const href = router.resolve({
|
||||
name: 'vnc-console',
|
||||
params: { cfgId: cfgId.value, instanceId: instanceId.value },
|
||||
query: { region: region.value, name: instance.data.value?.displayName },
|
||||
}).href
|
||||
window.open(href, '_blank')
|
||||
}
|
||||
|
||||
function openSerial() {
|
||||
void consoleStore.openSerial({
|
||||
cfgId: cfgId.value,
|
||||
instanceId: instanceId.value,
|
||||
region: region.value,
|
||||
instanceName: instance.data.value?.displayName,
|
||||
rootPassword: rootPassword.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function doRename(name: string) {
|
||||
renaming.value = true
|
||||
try {
|
||||
await updateInstance(cfgId.value, instanceId.value, { displayName: name }, region.value)
|
||||
message.success('已重命名')
|
||||
showRename.value = false
|
||||
void instance.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '重命名失败')
|
||||
} finally {
|
||||
renaming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function act(fn: () => Promise<unknown>, ok: string) {
|
||||
try {
|
||||
await fn()
|
||||
message.success(ok)
|
||||
void instance.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 换 IP 请求期间的按钮 loading,操作发出立即有反馈
|
||||
const changeIpBusy = ref(false)
|
||||
|
||||
function power(action: PowerAction) {
|
||||
void act(
|
||||
() => instanceAction(cfgId.value, instanceId.value, action, region.value),
|
||||
`${action} 已提交`,
|
||||
)
|
||||
}
|
||||
|
||||
const moreOptions = [
|
||||
{ label: 'SOFTSTOP(系统内关机)', key: 'SOFTSTOP' },
|
||||
{ label: 'SOFTRESET(系统内重启)', key: 'SOFTRESET' },
|
||||
]
|
||||
|
||||
function terminate() {
|
||||
dialog.warning({
|
||||
title: '终止实例',
|
||||
content: '终止后实例不可恢复。是否同时保留引导卷?',
|
||||
positiveText: '终止并删除引导卷',
|
||||
negativeText: '终止但保留引导卷',
|
||||
onPositiveClick: () =>
|
||||
act(
|
||||
() => terminateInstance(cfgId.value, instanceId.value, false, region.value),
|
||||
'终止请求已提交',
|
||||
),
|
||||
onNegativeClick: () =>
|
||||
act(
|
||||
() => terminateInstance(cfgId.value, instanceId.value, true, region.value),
|
||||
'终止请求已提交(保留引导卷)',
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
async function doChangeIp() {
|
||||
changeIpBusy.value = true
|
||||
await act(() => changePublicIp(cfgId.value, instanceId.value, region.value), '公网 IP 已更换')
|
||||
changeIpBusy.value = false
|
||||
}
|
||||
|
||||
const volColumns: DataTableColumns<VolumeAttachment> = [
|
||||
{
|
||||
title: '块卷',
|
||||
key: 'volumeName',
|
||||
minWidth: 170,
|
||||
render: (r) =>
|
||||
h('div', { class: 'min-w-0' }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'font-medium' },
|
||||
r.volumeName || r.displayName || truncOcid(r.volumeId, 18),
|
||||
),
|
||||
r.volumeId.includes('.bootvolume.')
|
||||
? h('div', { class: 'text-xs text-ink-3 mt-px' }, '引导卷 · 作数据卷附加')
|
||||
: null,
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '设备路径',
|
||||
key: 'device',
|
||||
minWidth: 200,
|
||||
render: (r) => h('span', { class: 'mono' }, r.device || '—'),
|
||||
},
|
||||
{
|
||||
title: '读写',
|
||||
key: 'isReadOnly',
|
||||
width: 80,
|
||||
render: (r) => h('span', { class: 'text-[13px]' }, r.isReadOnly ? '只读' : '读写'),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
width: 120,
|
||||
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 90,
|
||||
render: (r) =>
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () =>
|
||||
act(() => detachVolume(cfgId.value, r.id, region.value), '分离请求已提交'),
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '分离' }),
|
||||
default: () => '分离该块卷?OS 内请先卸载文件系统',
|
||||
},
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
/** DELETED 是 OCI 保留的终态记录,不可操作也无展示价值,直接过滤 */
|
||||
const liveConsoles = computed(() =>
|
||||
(consoles.data.value ?? []).filter((c) => c.lifecycleState !== 'DELETED'),
|
||||
)
|
||||
|
||||
const deletingConn = ref('')
|
||||
|
||||
async function doDeleteConsoleConn(id: string) {
|
||||
deletingConn.value = id
|
||||
try {
|
||||
await deleteConsoleConnection(cfgId.value, id, region.value)
|
||||
message.success('删除请求已提交')
|
||||
void consoles.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败')
|
||||
} finally {
|
||||
deletingConn.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 连接串行:类型标签 + 截断命令 + 行内复制,避免整串撑爆表格 */
|
||||
function connStringRow(label: string, value: string): ReturnType<typeof h> {
|
||||
return h('div', { class: 'flex min-w-0 max-w-[640px] items-center gap-1.5' }, [
|
||||
h('span', { class: 'shrink-0 rounded bg-wash px-1.5 text-[11px] leading-5 text-ink-2' }, label),
|
||||
h('code', { class: 'mono min-w-0 flex-1 truncate text-xs', title: value }, value),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
onClick: async () => {
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success(`已复制${label}连接串`)
|
||||
},
|
||||
},
|
||||
{ default: () => '复制' },
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
width: 110,
|
||||
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
|
||||
},
|
||||
{
|
||||
title: '连接串',
|
||||
key: 'connectionString',
|
||||
minWidth: 320,
|
||||
render: (r) =>
|
||||
h('div', { class: 'flex min-w-0 flex-col gap-0.5' }, [
|
||||
connStringRow('串口', r.connectionString),
|
||||
connStringRow('VNC', r.vncConnectionString),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 90,
|
||||
render: (r) =>
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => doDeleteConsoleConn(r.id) },
|
||||
{
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
type: 'error',
|
||||
loading: deletingConn.value === r.id,
|
||||
disabled: deletingConn.value !== '' && deletingConn.value !== r.id,
|
||||
},
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
default: () => '删除该控制台连接?',
|
||||
},
|
||||
),
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
||||
<div
|
||||
v-if="instance.loading.value && !instance.data.value"
|
||||
class="panel flex items-center justify-center py-20"
|
||||
>
|
||||
<NSpin size="small" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="instance.error.value"
|
||||
class="panel px-5 py-10 text-center text-[13px] text-ink-3"
|
||||
>
|
||||
{{ instance.error.value }}
|
||||
</div>
|
||||
|
||||
<div v-if="instance.data.value" class="panel px-5 py-4">
|
||||
<RouterLink
|
||||
class="mb-2 inline-flex items-center gap-1.5 text-[13px] text-ink-3 hover:text-ink"
|
||||
:to="{ name: 'instances' }"
|
||||
>
|
||||
← 返回实例列表
|
||||
</RouterLink>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h1 class="page-title">{{ instance.data.value.displayName }}</h1>
|
||||
<LifecycleBadge :state="instance.data.value.lifecycleState" />
|
||||
<div class="flex-1" />
|
||||
<NButton v-if="isStopped" size="small" :disabled="isEnded" @click="power('START')">
|
||||
启动
|
||||
</NButton>
|
||||
<NButton v-else size="small" :disabled="isEnded" @click="power('STOP')">停止</NButton>
|
||||
<NButton size="small" :disabled="isEnded" @click="power('RESET')">重启</NButton>
|
||||
<NButton size="small" type="error" ghost :disabled="isEnded" @click="terminate">
|
||||
终止
|
||||
</NButton>
|
||||
<NDropdown
|
||||
:options="moreOptions"
|
||||
:disabled="isEnded"
|
||||
@select="(key: string) => power(key as PowerAction)"
|
||||
>
|
||||
<NButton size="small" quaternary :disabled="isEnded">⋯</NButton>
|
||||
</NDropdown>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-5 gap-4 max-lg:grid-cols-3 max-md:grid-cols-1">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">租户</div>
|
||||
<div class="mt-0.5 truncate text-[13px]" :title="config.data.value?.tenancyName">
|
||||
{{ config.data.value?.tenancyName || config.data.value?.alias || '…' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">区域 / AD</div>
|
||||
<div class="mt-0.5 truncate text-[13px]">
|
||||
{{ regionAlias(instance.data.value.region) }} ·
|
||||
{{ shortAd(instance.data.value.availabilityDomain) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">Shape</div>
|
||||
<div class="mono mt-0.5 truncate text-[13px]">{{ instance.data.value.shape }}</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">规格</div>
|
||||
<div class="mt-0.5 text-[13px]">
|
||||
{{ instance.data.value.ocpus }} OCPU · {{ instance.data.value.memoryInGBs }} GB
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">创建时间</div>
|
||||
<div class="mt-0.5 text-[13px]">{{ fmtTime(instance.data.value.timeCreated) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
|
||||
<!-- 基本信息 -->
|
||||
<div v-if="instance.data.value" class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">基本信息</div>
|
||||
<NButton size="tiny" quaternary :disabled="isEnded" @click="showRename = true">
|
||||
重命名
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex flex-col px-4.5 py-1.5">
|
||||
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">OCID</div>
|
||||
<div class="min-w-0 flex-1"><OcidText :ocid="instance.data.value.id" /></div>
|
||||
</div>
|
||||
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">镜像</div>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<span class="truncate text-[13px]" :title="image.data.value?.displayName">
|
||||
{{ image.data.value?.displayName ?? (image.loading.value ? '…' : '未知镜像') }}
|
||||
</span>
|
||||
<NButton size="tiny" quaternary type="primary" @click="copyImageOcid">复制</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="rootPassword"
|
||||
class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5"
|
||||
>
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">ROOT 密码</div>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<code class="mono truncate text-[13px]" :title="rootPassword">{{ rootPassword }}</code>
|
||||
<NButton size="tiny" quaternary type="primary" @click="copyRootPassword">
|
||||
复制
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">规格调整</div>
|
||||
<div class="min-w-0 flex-1 text-[13px]">
|
||||
{{ instance.data.value.ocpus }} OCPU · {{ instance.data.value.memoryInGBs }} GB
|
||||
<NButton
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="primary"
|
||||
:disabled="isEnded"
|
||||
@click="showResize = true"
|
||||
>
|
||||
调整
|
||||
</NButton>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
可更换 shape 或调整 Flex 规格 · 运行中调整会自动重启
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网络 -->
|
||||
<div v-if="instance.data.value" class="panel">
|
||||
<div class="border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">网络</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
主网卡地址快捷视图;全部网卡与各卡 IPv6 管理见下方「网卡(VNIC)」
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col px-4.5 py-1.5">
|
||||
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">公网 IP</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="mono">{{ instance.data.value.publicIp || '—' }}</span>
|
||||
<NPopconfirm @positive-click="doChangeIp">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="primary"
|
||||
:disabled="isEnded"
|
||||
:loading="changeIpBusy"
|
||||
>
|
||||
更换
|
||||
</NButton>
|
||||
</template>
|
||||
更换即删除旧临时公网 IP 再分配新的(保留 IP 会被拒绝),确认?
|
||||
</NPopconfirm>
|
||||
<div class="mt-0.5 text-xs text-ink-3">临时 IP · 更换即删旧分新</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">私有 IP</div>
|
||||
<div class="mono min-w-0 flex-1">{{ instance.data.value.privateIp || '—' }}</div>
|
||||
</div>
|
||||
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">IPv6</div>
|
||||
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
|
||||
<span
|
||||
v-for="addr in instance.data.value.ipv6Addresses"
|
||||
:key="addr"
|
||||
class="mono inline-flex max-w-full items-center rounded border border-line bg-white px-1.5 py-0.5 text-xs"
|
||||
>
|
||||
<span class="truncate">{{ addr }}</span>
|
||||
</span>
|
||||
<span v-if="!instance.data.value.ipv6Addresses?.length" class="text-[13px]">—</span>
|
||||
<div class="w-full text-xs text-ink-3">
|
||||
主网卡 IPv6 · 添加 / 删除在下方网卡(VNIC)区块按卡操作
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
|
||||
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">子网</div>
|
||||
<div class="min-w-0 flex-1"><OcidText :ocid="instance.data.value.subnetId" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网卡(VNIC):每卡一主行 + IPv6 副行;变化后刷实例(主网卡快捷视图跟随) -->
|
||||
<VnicPanel
|
||||
v-if="instance.data.value"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
:disabled="isEnded"
|
||||
@changed="instance.run({ silent: true })"
|
||||
/>
|
||||
|
||||
<!-- 引导卷 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">引导卷</div>
|
||||
<NButton
|
||||
size="small"
|
||||
:disabled="!isStopped || isEnded"
|
||||
:title="isStopped ? '' : '替换引导卷须实例处于 STOPPED'"
|
||||
@click="showReplaceBv = true"
|
||||
>
|
||||
替换引导卷
|
||||
</NButton>
|
||||
</div>
|
||||
<div
|
||||
v-if="(bvAtts.loading.value || bvDetails.loading.value) && !bvDetails.data.value"
|
||||
class="flex items-center justify-center py-8"
|
||||
>
|
||||
<NSpin size="small" />
|
||||
</div>
|
||||
<div v-else class="px-4.5 py-1.5">
|
||||
<div
|
||||
v-for="att in bvAtts.data.value ?? []"
|
||||
:key="att.id"
|
||||
class="flex flex-wrap items-center gap-3 border-b border-line-soft py-2.5 last:border-b-0"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-ink">
|
||||
{{ bvById.get(att.bootVolumeId)?.displayName ?? truncOcid(att.bootVolumeId, 26) }}
|
||||
</div>
|
||||
<div class="text-xs text-ink-3">
|
||||
<template v-if="bvById.get(att.bootVolumeId)">
|
||||
{{ bvById.get(att.bootVolumeId)!.sizeInGBs }} GB · VPU
|
||||
{{ vpusLabel(bvById.get(att.bootVolumeId)!.vpusPerGB) }}
|
||||
</template>
|
||||
<span v-else class="mono">{{ truncOcid(att.bootVolumeId, 26) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<LifecycleBadge :state="att.lifecycleState" />
|
||||
<div class="flex-1" />
|
||||
<NButton
|
||||
v-if="bvById.get(att.bootVolumeId)"
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="primary"
|
||||
:disabled="isEnded"
|
||||
@click="openEditBv(bvById.get(att.bootVolumeId)!)"
|
||||
>
|
||||
调整大小 / VPU
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
@positive-click="act(() => detachBootVolume(cfgId, att.id, region), '分离请求已提交')"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="tiny" quaternary :disabled="!isStopped || isEnded">分离</NButton>
|
||||
</template>
|
||||
分离引导卷?实例将无法启动直到挂载新引导卷
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
<div
|
||||
v-if="!(bvAtts.data.value ?? []).length"
|
||||
class="py-6 text-center text-[13px] text-ink-3"
|
||||
>
|
||||
无挂载引导卷
|
||||
</div>
|
||||
</div>
|
||||
<FootNote>
|
||||
大小仅支持扩容(在线生效,OS 内需扩展文件系统)· 挂载 / 分离 / 替换要求实例处于 STOPPED
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
<!-- 块存储卷 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">块存储卷</div>
|
||||
<NButton size="small" type="primary" :disabled="isEnded" @click="showAttachVol = true">
|
||||
附加块卷
|
||||
</NButton>
|
||||
</div>
|
||||
<NDataTable
|
||||
:columns="volColumns"
|
||||
:data="volAtts.data.value ?? []"
|
||||
:loading="volAtts.loading.value"
|
||||
:scroll-x="700"
|
||||
:row-key="(r: VolumeAttachment) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
半虚拟化附加,运行中实例可热插拔;附加后仍需在实例 OS 内分区挂载文件系统
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
<!-- 控制台连接 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">控制台连接(VNC / 串口)</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NButton size="small" type="primary" :disabled="isEnded" @click="openSerial">
|
||||
串行终端
|
||||
</NButton>
|
||||
<NButton size="small" :disabled="isEnded" @click="openVnc">网页 VNC</NButton>
|
||||
<NButton size="small" :disabled="isEnded" @click="showConsole = true">创建连接</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<NDataTable
|
||||
:columns="consoleColumns"
|
||||
:data="liveConsoles"
|
||||
:loading="consoles.loading.value"
|
||||
:scroll-x="640"
|
||||
:row-key="(r: ConsoleConnection) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
串行终端 / 网页 VNC 在浏览器内直连(创建会话时自动清理旧连接,二者同时只能开一个)·
|
||||
「创建连接」用于本地 ssh / VNC 客户端:公钥仅支持 RSA,串口直接执行串口连接串,VNC
|
||||
执行后客户端连 localhost:5900 · 串口登录需实例本地账户密码
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
<!-- 流量 -->
|
||||
<InstanceTrafficPanel
|
||||
v-if="instance.data.value"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
/>
|
||||
|
||||
<RenameModal
|
||||
v-model:show="showRename"
|
||||
title="重命名实例"
|
||||
:current="instance.data.value?.displayName ?? ''"
|
||||
:submitting="renaming"
|
||||
@submit="doRename"
|
||||
/>
|
||||
<ResizeModal
|
||||
v-if="instance.data.value"
|
||||
v-model:show="showResize"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
:shape="instance.data.value.shape"
|
||||
:ocpus="instance.data.value.ocpus"
|
||||
:memory-in-g-bs="instance.data.value.memoryInGBs"
|
||||
@resized="instance.run()"
|
||||
/>
|
||||
<ConsoleConnModal
|
||||
v-model:show="showConsole"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
@created="consoles.run()"
|
||||
/>
|
||||
<ReplaceBootVolumeModal
|
||||
v-if="instance.data.value"
|
||||
v-model:show="showReplaceBv"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
:availability-domain="instance.data.value.availabilityDomain"
|
||||
@replaced="bvAtts.run()"
|
||||
/>
|
||||
<AttachVolumeModal
|
||||
v-if="instance.data.value"
|
||||
v-model:show="showAttachVol"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
:availability-domain="instance.data.value.availabilityDomain"
|
||||
:attached-volume-ids="attachedVolumeIds"
|
||||
@attached="volAtts.run()"
|
||||
/>
|
||||
<EditBootVolumeModal
|
||||
v-model:show="showEditBv"
|
||||
:cfg-id="cfgId"
|
||||
:region="region"
|
||||
:boot-volume="editingBv"
|
||||
@updated="bvDetails.run()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user