Files
oci-portal-dash/src/components/tenant/QuotaTab.vue
T

243 lines
7.4 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 {
NCheckbox,
NDataTable,
NInput,
NInputGroup,
NInputGroupLabel,
NSelect,
type DataTableColumns,
} from 'naive-ui'
import { computed, h, reactive, ref, watch } from 'vue'
import { listCachedRegions } from '@/api/configs'
import { listLimitServices, listLimits } from '@/api/tenant'
import FootNote from '@/components/FootNote.vue'
import { regionCity, shortAd } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import type { LimitItem } from '@/types/api'
const props = defineProps<{ cfgId: number }>()
const service = ref('compute')
const name = ref('')
const region = ref('')
const scopeType = ref<'' | 'GLOBAL' | 'REGION' | 'AD'>('')
const withAvail = ref(false)
const hideZero = ref(false)
const scopeOptions = [
{ label: '全部作用域', value: '' },
{ label: 'GLOBAL', value: 'GLOBAL' },
{ label: 'REGION', value: 'REGION' },
{ label: 'AD', value: 'AD' },
]
const services = useAsync(() => listLimitServices(props.cfgId))
/** 区域选项来自服务端缓存(未开启多区域的租户只返回默认区域) */
const regions = useAsync(() => listCachedRegions(props.cfgId).catch(() => []))
const limits = useAsync(() =>
listLimits(props.cfgId, {
service: service.value,
name: name.value,
region: region.value || undefined,
scopeType: scopeType.value || undefined,
// 下推服务端过滤:0 配额不占「查用量」的 100 条并发名额
nonZero: hideZero.value || undefined,
withAvailability: withAvail.value,
}),
)
// 区域列表加载后默认选中主区域;已选值失效(退订)时同样回退
watch(
() => regions.data.value,
(subs) => {
if (!subs?.length) return
if (!region.value || !subs.some((s) => s.name === region.value))
region.value = subs.find((s) => s.isHomeRegion)?.name ?? subs[0].name
},
)
/** 选项用括号内城市短名(与全局选择器一致),避免选中框被长名截断 */
const regionOptions = computed(() =>
(regions.data.value ?? []).map((s) => ({
label: s.isHomeRegion ? `${regionCity(s.alias)} · 主` : regionCity(s.alias),
value: s.name,
})),
)
watch([service, withAvail, region, scopeType, hideZero], () => void limits.run())
const rows = computed(() =>
hideZero.value ? (limits.data.value ?? []).filter((r) => r.value !== 0) : (limits.data.value ?? []),
)
const serviceOptions = computed(
() =>
services.data.value?.map((s) => ({ label: s.description || s.name, value: s.name })) ?? [
{ label: 'Compute', value: 'compute' },
],
)
function usagePct(row: LimitItem): number | null {
if (row.used === undefined || row.value === 0) return null
return Math.round((row.used / row.value) * 100)
}
/** 受控分页:字面量对象会在重渲染时被重建导致页大小切换失效 */
const pagination = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onUpdatePage: (p: number) => {
pagination.page = p
},
onUpdatePageSize: (ps: number) => {
pagination.pageSize = ps
pagination.page = 1
},
})
const baseColumns: DataTableColumns<LimitItem> = [
{
title: '配额项',
key: 'name',
minWidth: 230,
render: (row) =>
h('div', [
h('div', { class: 'mono text-[12.5px] text-ink' }, row.name),
h(
'div',
{ class: 'text-xs text-ink-3 mt-px' },
row.availabilityDomain ? shortAd(row.availabilityDomain) : row.scopeType,
),
]),
},
{
title: '上限',
key: 'value',
width: 130,
render: (r) =>
h(
'span',
{ class: 'tabular-nums whitespace-nowrap' },
r.value.toLocaleString('en-US'),
),
},
]
/** 字节类配额可达百亿级,列宽须容纳千分位全量数值,禁止折行 */
const availColumns: DataTableColumns<LimitItem> = [
{
title: '已用',
key: 'used',
width: 120,
render: (r) =>
h(
'span',
{ class: 'tabular-nums whitespace-nowrap' },
r.used === undefined ? '—' : r.used.toLocaleString('en-US'),
),
},
{
title: '可用',
key: 'available',
width: 120,
render: (r) =>
h(
'span',
{ class: 'tabular-nums whitespace-nowrap' },
r.available === undefined ? '—' : r.available.toLocaleString('en-US'),
),
},
{
title: '用量',
key: 'pct',
minWidth: 160,
render: (row) => {
const pct = usagePct(row)
if (pct === null) return h('span', { class: 'text-xs text-ink-3' }, '无用量数据')
const full = pct >= 100
return h('div', { class: 'flex items-center gap-2' }, [
h('div', { class: 'h-1.5 w-24 overflow-hidden rounded-full bg-line-soft' }, [
h('div', {
class: 'h-full rounded-full',
style: { width: `${Math.min(pct, 100)}%`, background: full ? 'var(--color-err)' : 'var(--color-ok)' },
}),
]),
h(
'span',
{
class: 'text-[12.5px] tabular-nums',
style: full ? 'color:var(--color-err);font-weight:600' : 'color:var(--color-ink-2)',
},
`${pct}%`,
),
])
},
},
]
const columns = computed(() => (withAvail.value ? [...baseColumns, ...availColumns] : baseColumns))
</script>
<template>
<div class="panel">
<div class="flex flex-wrap items-center gap-2 px-4.5 py-3">
<div class="text-sm font-semibold">服务配额</div>
<div class="flex-1" />
<NInputGroup class="!w-60 max-md:!w-full">
<NInputGroupLabel size="small">区域</NInputGroupLabel>
<NSelect
v-model:value="region"
size="small"
filterable
:options="regionOptions"
:loading="regions.loading.value"
:consistent-menu-width="false"
/>
</NInputGroup>
<NInputGroup class="!w-72 max-lg:!w-60 max-md:!w-full">
<NInputGroupLabel size="small">服务</NInputGroupLabel>
<NSelect
v-model:value="service"
size="small"
filterable
:options="serviceOptions"
:loading="services.loading.value"
/>
</NInputGroup>
<NInputGroup class="!w-52 max-lg:!w-44 max-md:!w-full">
<NInputGroupLabel size="small">作用域</NInputGroupLabel>
<NSelect v-model:value="scopeType" size="small" :options="scopeOptions" />
</NInputGroup>
<NInputGroup class="!w-56 max-lg:!w-44 max-md:!w-full">
<NInputGroupLabel size="small">配额名</NInputGroupLabel>
<NInput
v-model:value="name"
size="small"
placeholder="如 core-count"
clearable
@keyup.enter="limits.run()"
@clear="limits.run()"
/>
</NInputGroup>
<NCheckbox v-model:checked="hideZero" size="small">忽略 0 配额</NCheckbox>
<NCheckbox v-model:checked="withAvail" size="small">查用量</NCheckbox>
</div>
<NDataTable
:columns="columns"
:data="rows"
:loading="limits.loading.value"
:pagination="pagination"
:scroll-x="withAvail ? 760 : 380"
:row-key="(r: LimitItem) => r.name + (r.availabilityDomain ?? '')"
/>
<FootNote>
勾选查用量后单次限 100 超出请用配额名 / 作用域过滤收窄配合忽略 0
配额」(服务端过滤可大幅释放名额不支持用量查询的配额自动跳过并显示无用量数据
</FootNote>
</div>
</template>