feat
This commit is contained in:
@@ -80,6 +80,8 @@ export interface HostHardwareDeviceUpsert {
|
||||
snmp_collect_enabled?: boolean
|
||||
redfish_collect_enabled?: boolean
|
||||
collect_interval?: number
|
||||
/** 是否启用监控调度 */
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface HostHardwareDeviceListPayload {
|
||||
@@ -191,9 +193,7 @@ export function isHostHardwareApiSuccess(res: HostHardwareApiEnvelope | null | u
|
||||
return false
|
||||
}
|
||||
|
||||
export function unwrapHostHardwareDetails<T>(
|
||||
res: (HostHardwareApiEnvelope<T> & { data?: T }) | null | undefined,
|
||||
): T | null {
|
||||
export function unwrapHostHardwareDetails<T>(res: (HostHardwareApiEnvelope<T> & { data?: T }) | null | undefined): T | null {
|
||||
if (!res || !isHostHardwareApiSuccess(res)) return null
|
||||
// 部分网关/SDK 将载荷放在 data 而非 details,与 logs 等模块一致做兼容
|
||||
return res.details ?? res.data ?? null
|
||||
@@ -203,15 +203,13 @@ export function unwrapHostHardwareDetails<T>(
|
||||
export function fetchHostHardwareLatestCollection(serverIdentity: string) {
|
||||
return request.get<HostHardwareApiEnvelope<HostHardwareLatestCollectionPayload>>(
|
||||
`${HW_PREFIX}/devices/by-server-identity/collection/latest`,
|
||||
{ params: { server_identity: serverIdentity } },
|
||||
{ params: { server_identity: serverIdentity } }
|
||||
)
|
||||
}
|
||||
|
||||
/** 设备详情(含最新一条 status) */
|
||||
export function fetchHostHardwareDevice(deviceId: string) {
|
||||
return request.get<HostHardwareApiEnvelope<HostHardwareDeviceDetailPayload>>(
|
||||
`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`,
|
||||
)
|
||||
return request.get<HostHardwareApiEnvelope<HostHardwareDeviceDetailPayload>>(`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`)
|
||||
}
|
||||
|
||||
/** 分页列表(可按 type / status / asset_id 筛选) */
|
||||
@@ -234,10 +232,7 @@ export function createHostHardwareDevice(data: HostHardwareDeviceUpsert) {
|
||||
|
||||
/** 更新设备(全量必填字段;密码留空则不修改) */
|
||||
export function updateHostHardwareDevice(deviceId: string, data: HostHardwareDeviceUpsert) {
|
||||
return request.put<HostHardwareApiEnvelope<HostHardwareDevice>>(
|
||||
`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`,
|
||||
data,
|
||||
)
|
||||
return request.put<HostHardwareApiEnvelope<HostHardwareDevice>>(`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`, data)
|
||||
}
|
||||
|
||||
/** 异步立即采集 */
|
||||
@@ -256,12 +251,7 @@ export function disableHostHardwareDevice(deviceId: string) {
|
||||
}
|
||||
|
||||
/** 单指标历史曲线(JWT) */
|
||||
export function fetchHostHardwareMetricHistory(
|
||||
deviceId: string,
|
||||
metricName: string,
|
||||
startTime?: string,
|
||||
endTime?: string,
|
||||
) {
|
||||
export function fetchHostHardwareMetricHistory(deviceId: string, metricName: string, startTime?: string, endTime?: string) {
|
||||
return request.get<HostHardwareApiEnvelope<HostHardwareMetricHistoryPayload>>(
|
||||
`${HW_PREFIX}/metrics/devices/${encodeURIComponent(deviceId)}/history`,
|
||||
{
|
||||
@@ -270,26 +260,19 @@ export function fetchHostHardwareMetricHistory(
|
||||
...(startTime ? { start_time: startTime } : {}),
|
||||
...(endTime ? { end_time: endTime } : {}),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 日汇总统计 */
|
||||
export function fetchHostHardwareStatistics(
|
||||
deviceId: string,
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
) {
|
||||
return request.get<HostHardwareApiEnvelope<HostHardwareStatisticsRow[]>>(
|
||||
`${HW_PREFIX}/metrics/statistics`,
|
||||
{
|
||||
export function fetchHostHardwareStatistics(deviceId: string, startDate?: string, endDate?: string) {
|
||||
return request.get<HostHardwareApiEnvelope<HostHardwareStatisticsRow[]>>(`${HW_PREFIX}/metrics/statistics`, {
|
||||
params: {
|
||||
device_id: deviceId,
|
||||
...(startDate ? { start_date: startDate } : {}),
|
||||
...(endDate ? { end_date: endDate } : {}),
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,7 +280,7 @@ export function fetchHostHardwareStatistics(
|
||||
*/
|
||||
export function normalizeHostHardwareMetrics(
|
||||
status: HostHardwareStatus | null | undefined,
|
||||
metricsFromApi: HostHardwareMetricsRow[] | null | undefined,
|
||||
metricsFromApi: HostHardwareMetricsRow[] | null | undefined
|
||||
): NormalizedHostHardwareMetric[] {
|
||||
const fromApi = metricsFromApi ?? []
|
||||
if (fromApi.length > 0) {
|
||||
|
||||
@@ -11,10 +11,9 @@
|
||||
unmount-on-close
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="(v: boolean) => emit('update:visible', v)"
|
||||
>
|
||||
<div class="modal-scroll">
|
||||
<a-spin :loading="loading" style="width: 100%; min-height: 200px">
|
||||
<a-spin :loading="loading" style="width: 98%; margin: 0 auto; min-height: 200px">
|
||||
<div v-if="!loading && blockedNoIdentity" class="blocked-wrap">
|
||||
<a-alert type="warning" show-icon>
|
||||
当前服务器未配置唯一标识。请先在「编辑服务器」中填写并保存,以便与 DC-Hardware 带外设备关联。
|
||||
@@ -22,17 +21,22 @@
|
||||
</div>
|
||||
|
||||
<template v-else-if="!loading">
|
||||
|
||||
|
||||
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
|
||||
<a-collapse :default-active-key="['base', 'proto', 'collect']" :bordered="false">
|
||||
<a-collapse-item key="base" header="基础信息">
|
||||
<a-divider orientation="left">基础信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="管理 IP(BMC / 带外)">
|
||||
<span class="readonly-field">{{ managementIp || '—' }}</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="collect_interval" label="采集间隔(秒)">
|
||||
<a-input-number v-model="form.collect_interval" :min="0" style="width: 100%" />
|
||||
<template #extra>
|
||||
<span class="form-extra">0 或留空表示使用服务默认间隔</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item field="description" label="备注">
|
||||
<a-textarea v-model="form.description" :rows="2" placeholder="可选" allow-clear />
|
||||
@@ -44,15 +48,10 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-item>
|
||||
|
||||
<a-collapse-item key="proto" header="采集协议与连接">
|
||||
<a-divider orientation="left">采集协议与连接</a-divider>
|
||||
<a-form-item field="protocol" label="协议" required>
|
||||
<a-radio-group
|
||||
:model-value="form.protocol"
|
||||
type="button"
|
||||
@update:model-value="onProtocolModelUpdate"
|
||||
>
|
||||
<a-radio-group :model-value="form.protocol" type="button" @update:model-value="onProtocolModelUpdate">
|
||||
<a-radio value="ipmi">IPMI</a-radio>
|
||||
<a-radio value="snmp">SNMP</a-radio>
|
||||
<a-radio value="redfish">Redfish</a-radio>
|
||||
@@ -62,7 +61,6 @@
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<!-- IPMI / Redfish 共用账号 -->
|
||||
<template v-if="form.protocol === 'ipmi' || form.protocol === 'redfish'">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
@@ -139,11 +137,7 @@
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item field="redfish_base_url" label="Redfish 根 URL">
|
||||
<a-input
|
||||
v-model="form.redfish_base_url"
|
||||
placeholder="留空则默认 https://{管理IP}/redfish/v1"
|
||||
allow-clear
|
||||
/>
|
||||
<a-input v-model="form.redfish_base_url" placeholder="留空则默认 https://{管理IP}/redfish/v1" allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
@@ -164,52 +158,21 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-collapse-item>
|
||||
|
||||
<a-collapse-item key="collect" header="采集调度与扩展">
|
||||
<a-divider orientation="left">扩展配置</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="collect_interval" label="采集间隔(秒)">
|
||||
<a-input-number v-model="form.collect_interval" :min="0" style="width: 100%" />
|
||||
<template #extra>
|
||||
<span class="form-extra">0 或留空表示使用服务默认间隔(YAML)</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isEdit && hwEnabled !== undefined" :span="12">
|
||||
<a-form-item label="监控开关(只读)">
|
||||
<a-space>
|
||||
<a-tag :color="hwEnabled ? 'green' : 'gray'">{{ hwEnabled ? '已启用' : '已禁用' }}</a-tag>
|
||||
<span class="form-extra">保存后可用下方快捷操作切换</span>
|
||||
</a-space>
|
||||
<a-form-item label="启用监控">
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item field="extra_config" label="扩展配置(JSON 字符串)">
|
||||
<a-textarea
|
||||
v-model="form.extra_config"
|
||||
:rows="4"
|
||||
placeholder='可选,非空须为合法 JSON,如 {"key":"value"}'
|
||||
allow-clear
|
||||
/>
|
||||
<a-textarea v-model="form.extra_config" :rows="4" placeholder='可选,非空须为合法 JSON,如 {"key":"value"}' allow-clear />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-item>
|
||||
</a-collapse>
|
||||
|
||||
<a-divider v-if="isEdit && deviceId" orientation="left">快捷操作</a-divider>
|
||||
<a-space v-if="isEdit && deviceId" wrap>
|
||||
<a-button type="outline" :loading="actionLoading === 'collect'" @click="doCollect">
|
||||
立即采集
|
||||
</a-button>
|
||||
<a-button v-if="!hwEnabled" type="outline" status="success" :loading="actionLoading === 'enable'" @click="doEnable">
|
||||
启用监控
|
||||
</a-button>
|
||||
<a-button v-else type="outline" status="warning" :loading="actionLoading === 'disable'" @click="doDisable">
|
||||
禁用监控
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</template>
|
||||
</a-spin>
|
||||
@@ -228,9 +191,6 @@ import {
|
||||
fetchHostHardwareDeviceList,
|
||||
createHostHardwareDevice,
|
||||
updateHostHardwareDevice,
|
||||
triggerHostHardwareCollect,
|
||||
enableHostHardwareDevice,
|
||||
disableHostHardwareDevice,
|
||||
isHostHardwareApiSuccess,
|
||||
unwrapHostHardwareDetails,
|
||||
type HostHardwareDevice,
|
||||
@@ -250,8 +210,6 @@ const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const blockedNoIdentity = ref(false)
|
||||
const deviceId = ref<string | null>(null)
|
||||
const hwEnabled = ref<boolean | undefined>(undefined)
|
||||
const actionLoading = ref<'collect' | 'enable' | 'disable' | ''>('')
|
||||
|
||||
/** 防止多次触发 loadHardware 时,较早返回的请求覆盖用户已切换的协议/表单 */
|
||||
let hardwareLoadGeneration = 0
|
||||
@@ -285,13 +243,9 @@ const managementIp = computed(() => {
|
||||
const hardwareType = ref('server')
|
||||
const hardwareAssetId = ref('')
|
||||
|
||||
const passwordPlaceholder = computed(() =>
|
||||
isEdit.value ? '留空则不修改已保存的密码' : 'BMC / Redfish 密码',
|
||||
)
|
||||
const passwordPlaceholder = computed(() => (isEdit.value ? '留空则不修改已保存的密码' : 'BMC / Redfish 密码'))
|
||||
|
||||
const passwordHint = computed(() =>
|
||||
isEdit.value ? '编辑时留空将保留原密码' : '按机房安全要求妥善保管',
|
||||
)
|
||||
const passwordHint = computed(() => (isEdit.value ? '编辑时留空将保留原密码' : '按机房安全要求妥善保管'))
|
||||
|
||||
const form = reactive({
|
||||
protocol: 'ipmi' as 'ipmi' | 'snmp' | 'redfish',
|
||||
@@ -313,6 +267,7 @@ const form = reactive({
|
||||
snmp_collect_enabled: true,
|
||||
redfish_collect_enabled: true,
|
||||
collect_interval: 0,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
const rules: Record<string, FieldRule | FieldRule[]> = {
|
||||
@@ -341,6 +296,7 @@ function resetForm() {
|
||||
form.snmp_collect_enabled = true
|
||||
form.redfish_collect_enabled = true
|
||||
form.collect_interval = 0
|
||||
form.enabled = true
|
||||
}
|
||||
|
||||
function applyFromServer(r: ServerItem) {
|
||||
@@ -372,7 +328,7 @@ function applyFromDevice(d: HostHardwareDevice) {
|
||||
form.snmp_collect_enabled = d.snmp_collect_enabled !== false
|
||||
form.redfish_collect_enabled = d.redfish_collect_enabled !== false
|
||||
form.collect_interval = d.collect_interval ?? 0
|
||||
hwEnabled.value = d.enabled
|
||||
form.enabled = d.enabled !== false
|
||||
}
|
||||
|
||||
function validateExtraConfigJson(): boolean {
|
||||
@@ -417,6 +373,7 @@ function buildPayload(): HostHardwareDeviceUpsert {
|
||||
snmp_collect_enabled: form.snmp_collect_enabled,
|
||||
redfish_collect_enabled: form.redfish_collect_enabled,
|
||||
collect_interval: form.collect_interval,
|
||||
enabled: form.enabled,
|
||||
}
|
||||
const pw = form.password?.trim()
|
||||
if (pw) {
|
||||
@@ -454,7 +411,6 @@ async function loadDeviceByServerIdentityFallback(sid: string, gen: number): Pro
|
||||
async function loadHardware() {
|
||||
const gen = ++hardwareLoadGeneration
|
||||
deviceId.value = null
|
||||
hwEnabled.value = undefined
|
||||
blockedNoIdentity.value = false
|
||||
if (!props.record) {
|
||||
return
|
||||
@@ -474,6 +430,7 @@ async function loadHardware() {
|
||||
if (isHostHardwareApiSuccess(colRes)) {
|
||||
const col = unwrapHostHardwareDetails(colRes)
|
||||
const did = col?.device_id
|
||||
deviceId.value = col?.device_id
|
||||
if (did) {
|
||||
const detailOk = await loadDeviceDetailIntoForm(did, gen)
|
||||
if (gen !== hardwareLoadGeneration) return
|
||||
@@ -516,7 +473,7 @@ watch(
|
||||
deviceId.value = null
|
||||
blockedNoIdentity.value = false
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
async function handleSubmit() {
|
||||
@@ -531,6 +488,7 @@ async function handleSubmit() {
|
||||
if (!validateExtraConfigJson()) return
|
||||
|
||||
const r = props.record
|
||||
console.log('r,', r)
|
||||
if (!(r?.name || '').trim()) {
|
||||
Message.error('服务器名称为空,请先在编辑服务器中填写名称')
|
||||
return
|
||||
@@ -541,13 +499,14 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
console.log('deviceId.value,', deviceId.value)
|
||||
try {
|
||||
const payload = buildPayload()
|
||||
let res: { code?: number | string; message?: string }
|
||||
if (deviceId.value) {
|
||||
res = await updateHostHardwareDevice(deviceId.value, payload) as typeof res
|
||||
res = (await updateHostHardwareDevice(deviceId.value, payload)) as typeof res
|
||||
} else {
|
||||
res = await createHostHardwareDevice(payload) as typeof res
|
||||
res = (await createHostHardwareDevice(payload)) as typeof res
|
||||
}
|
||||
if (isHostHardwareApiSuccess(res)) {
|
||||
Message.success(deviceId.value ? '修改成功' : '保存成功')
|
||||
@@ -557,8 +516,9 @@ async function handleSubmit() {
|
||||
Message.error((res as { message?: string }).message || '保存失败')
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const msg = (e as { response?: { data?: { message?: string } }; message?: string })?.response?.data?.message
|
||||
|| (e as { message?: string })?.message
|
||||
const msg =
|
||||
(e as { response?: { data?: { message?: string } }; message?: string })?.response?.data?.message ||
|
||||
(e as { message?: string })?.message
|
||||
Message.error(msg || '保存失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
@@ -568,59 +528,6 @@ async function handleSubmit() {
|
||||
function handleCancel() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
async function doCollect() {
|
||||
if (!deviceId.value) return
|
||||
actionLoading.value = 'collect'
|
||||
try {
|
||||
const res = await triggerHostHardwareCollect(deviceId.value)
|
||||
if (isHostHardwareApiSuccess(res)) {
|
||||
Message.success('采集任务已启动')
|
||||
} else {
|
||||
Message.error((res as { message?: string }).message || '操作失败')
|
||||
}
|
||||
} catch {
|
||||
Message.error('请求失败')
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function doEnable() {
|
||||
if (!deviceId.value) return
|
||||
actionLoading.value = 'enable'
|
||||
try {
|
||||
const res = await enableHostHardwareDevice(deviceId.value)
|
||||
if (isHostHardwareApiSuccess(res)) {
|
||||
Message.success('已启用监控')
|
||||
hwEnabled.value = true
|
||||
} else {
|
||||
Message.error((res as { message?: string }).message || '操作失败')
|
||||
}
|
||||
} catch {
|
||||
Message.error('请求失败')
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function doDisable() {
|
||||
if (!deviceId.value) return
|
||||
actionLoading.value = 'disable'
|
||||
try {
|
||||
const res = await disableHostHardwareDevice(deviceId.value)
|
||||
if (isHostHardwareApiSuccess(res)) {
|
||||
Message.success('已禁用监控')
|
||||
hwEnabled.value = false
|
||||
} else {
|
||||
Message.error((res as { message?: string }).message || '操作失败')
|
||||
}
|
||||
} catch {
|
||||
Message.error('请求失败')
|
||||
} finally {
|
||||
actionLoading.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user