初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NIcon,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NPopconfirm,
|
||||
NSelect,
|
||||
useMessage,
|
||||
type DataTableColumns,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { deleteBootVolume, listBootVolumes } from '@/api/bootVolumes'
|
||||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import AttachBootVolumeModal from '@/components/storage/AttachBootVolumeModal.vue'
|
||||
import EditBootVolumeModal from '@/components/storage/EditBootVolumeModal.vue'
|
||||
import { fmtTime, shortAd, truncOcid, vpusLabel } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
import type { BootVolume } from '@/types/api'
|
||||
|
||||
interface BvRow extends BootVolume {
|
||||
cfgId: number
|
||||
}
|
||||
|
||||
/** 租户 / 区域 / 区间来自侧栏全局作用域 */
|
||||
const scope = useScopeStore()
|
||||
const message = useMessage()
|
||||
|
||||
/** 当前作用域租户失联时整页替换占位,不发起资源请求 */
|
||||
const isDead = computed(() => scope.currentConfig?.aliveStatus === 'dead')
|
||||
|
||||
const rows = useAsync<BvRow[]>(async () => {
|
||||
const cfg = scope.currentConfig
|
||||
if (!cfg || cfg.aliveStatus === 'dead') return []
|
||||
try {
|
||||
const list = await listBootVolumes(
|
||||
cfg.id,
|
||||
undefined,
|
||||
scope.compartmentId || undefined,
|
||||
scope.region || undefined,
|
||||
)
|
||||
return (list ?? []).map((item) => ({ ...item, cfgId: cfg.id }))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}, false)
|
||||
|
||||
watch(
|
||||
[() => scope.currentConfig?.id, () => scope.region, () => scope.compartmentId],
|
||||
() => void rows.run(),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 挂载 / 分离是异步动作,提交后延时补一次刷新 */
|
||||
function refreshSoon() {
|
||||
void rows.run()
|
||||
setTimeout(() => void rows.run(), 6000)
|
||||
}
|
||||
|
||||
const showEdit = ref(false)
|
||||
const showAttach = ref(false)
|
||||
const current = ref<BvRow | null>(null)
|
||||
|
||||
function openEdit(row: BvRow) {
|
||||
current.value = row
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
function openAttach(row: BvRow) {
|
||||
current.value = row
|
||||
showAttach.value = true
|
||||
}
|
||||
|
||||
async function remove(row: BvRow) {
|
||||
try {
|
||||
await deleteBootVolume(row.cfgId, row.id, scope.region || undefined)
|
||||
message.success('引导卷已删除')
|
||||
void rows.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyImageOcid(row: BvRow) {
|
||||
await navigator.clipboard.writeText(row.imageId)
|
||||
message.success('已复制镜像 OCID')
|
||||
}
|
||||
|
||||
function renderCopyIcon() {
|
||||
return h(
|
||||
'svg',
|
||||
{ viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.5' },
|
||||
[
|
||||
h('rect', { x: '9', y: '9', width: '13', height: '13', rx: '2' }),
|
||||
h('path', { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/** 名称行 + 来源镜像行(镜像名展示,按钮复制镜像 OCID) */
|
||||
function renderName(row: BvRow) {
|
||||
const children = [h('div', { class: 'font-medium' }, row.displayName)]
|
||||
if (row.imageId)
|
||||
children.push(
|
||||
h('div', { class: 'mt-px flex min-w-0 items-center gap-0.5 text-xs text-ink-3' }, [
|
||||
h(
|
||||
'span',
|
||||
{ class: 'truncate', title: row.imageName || row.imageId },
|
||||
row.imageName || truncOcid(row.imageId, 20),
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
'aria-label': '复制镜像 OCID',
|
||||
onClick: () => copyImageOcid(row),
|
||||
},
|
||||
{ icon: () => h(NIcon, { size: 13 }, { default: renderCopyIcon }) },
|
||||
),
|
||||
]),
|
||||
)
|
||||
return h('div', { class: 'min-w-0' }, children)
|
||||
}
|
||||
|
||||
function renderActions(row: BvRow) {
|
||||
const attached = Boolean(row.attachedInstanceId)
|
||||
const gone = row.lifecycleState === 'TERMINATED' || row.lifecycleState === 'TERMINATING'
|
||||
const attachBtn = h(
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
tertiary: true,
|
||||
disabled: gone || attached || row.lifecycleState !== 'AVAILABLE',
|
||||
title: attached ? '已挂载到实例' : undefined,
|
||||
onClick: () => openAttach(row),
|
||||
},
|
||||
{ default: () => '挂载' },
|
||||
)
|
||||
const deleteBtn =
|
||||
attached || gone
|
||||
? h(
|
||||
NButton,
|
||||
{ size: 'tiny', type: 'error', ghost: true, disabled: true, title: attached ? '仅未挂载状态可删除' : undefined },
|
||||
{ default: () => '删除' },
|
||||
)
|
||||
: h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => remove(row) },
|
||||
{
|
||||
trigger: () =>
|
||||
h(NButton, { size: 'tiny', type: 'error', ghost: true }, { default: () => '删除' }),
|
||||
default: () => '删除该引导卷?数据不可恢复',
|
||||
},
|
||||
)
|
||||
return h('div', { class: 'flex items-center gap-1.5' }, [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'tiny', tertiary: true, disabled: gone, onClick: () => openEdit(row) },
|
||||
{ default: () => '编辑' },
|
||||
),
|
||||
attachBtn,
|
||||
deleteBtn,
|
||||
])
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumns<BvRow>>(() => [
|
||||
{ title: '引导卷', key: 'displayName', minWidth: 250, render: renderName },
|
||||
{
|
||||
title: 'AD',
|
||||
key: 'availabilityDomain',
|
||||
width: 80,
|
||||
render: (r) => h('span', { class: 'text-[13px]' }, shortAd(r.availabilityDomain)),
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
key: 'sizeInGBs',
|
||||
width: 90,
|
||||
render: (r) => h('span', { class: 'tabular-nums' }, `${r.sizeInGBs} GB`),
|
||||
},
|
||||
{
|
||||
title: '性能',
|
||||
key: 'vpusPerGB',
|
||||
width: 120,
|
||||
render: (r) => h('span', { class: 'text-[13px]' }, vpusLabel(r.vpusPerGB)),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
width: 116,
|
||||
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
|
||||
},
|
||||
{
|
||||
title: '挂载状态',
|
||||
key: 'attached',
|
||||
width: 100,
|
||||
render: (r) =>
|
||||
h(StatusBadge, {
|
||||
kind: r.attachedInstanceId ? 'run' : 'check',
|
||||
label: r.attachedInstanceId ? '已挂载' : '未挂载',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '挂载实例',
|
||||
key: 'attachedInstanceId',
|
||||
minWidth: 150,
|
||||
render: (r) =>
|
||||
r.attachedInstanceId
|
||||
? h(
|
||||
RouterLink,
|
||||
{
|
||||
class: 'text-[13px] font-medium text-accent hover:text-accent-hover',
|
||||
to: {
|
||||
name: 'instance-detail',
|
||||
params: { cfgId: r.cfgId, instanceId: r.attachedInstanceId },
|
||||
query: { region: scope.region || undefined },
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
r.attachedInstanceName || r.displayName.replace(' (Boot Volume)', ''),
|
||||
},
|
||||
)
|
||||
: h('span', { class: 'text-ink-3' }, '—'),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'timeCreated',
|
||||
width: 140,
|
||||
render: (r) => h('span', { class: 'text-[13px] text-ink-2' }, fmtTime(r.timeCreated)),
|
||||
},
|
||||
{ title: '操作', key: 'actions', width: 190, render: renderActions },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h1 class="page-title">引导卷</h1>
|
||||
<span class="text-[13px] text-ink-3">
|
||||
{{ rows.data.value?.length ?? '…' }} 个 ·
|
||||
{{ scope.currentConfig?.alias ?? '…' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DeadPlaceholder v-if="isDead" :last-error="scope.currentConfig?.lastError" />
|
||||
<div v-else class="panel">
|
||||
<div class="flex flex-wrap items-center gap-2 px-4.5 py-3">
|
||||
<NInputGroup class="!w-72 max-md:!w-full">
|
||||
<NInputGroupLabel size="small">区间</NInputGroupLabel>
|
||||
<NSelect
|
||||
v-model:value="scope.compartmentId"
|
||||
:options="scope.compOptions"
|
||||
:loading="scope.compartments.loading"
|
||||
:disabled="scope.compDisabled"
|
||||
size="small"
|
||||
/>
|
||||
</NInputGroup>
|
||||
</div>
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="rows.data.value ?? []"
|
||||
:loading="rows.loading.value"
|
||||
:scroll-x="1300"
|
||||
:row-key="(r: BvRow) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
引导卷随实例创建产生,不提供单独创建 · 挂载须实例同可用域且处于 STOPPED · 仅未挂载状态可删除
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
<EditBootVolumeModal
|
||||
v-model:show="showEdit"
|
||||
:cfg-id="current?.cfgId ?? null"
|
||||
:volume="current"
|
||||
:region="scope.region || undefined"
|
||||
@updated="rows.run()"
|
||||
/>
|
||||
<AttachBootVolumeModal
|
||||
v-model:show="showAttach"
|
||||
:cfg-id="current?.cfgId ?? null"
|
||||
:volume="current"
|
||||
:region="scope.region || undefined"
|
||||
@attached="refreshSoon()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user