feat: 展示通知 outbox 状态
- services: 增加 /notification-outbox 真实接口读取与摘要清洗 - store: 超级管理员加载最近通知 outbox - settings: 增加通知 outbox 运维表格
This commit is contained in:
@@ -25,6 +25,7 @@ import type {
|
||||
LoginPayload,
|
||||
LoginResult,
|
||||
MemberSummary,
|
||||
NotificationOutboxMessage,
|
||||
PipelineStep,
|
||||
PlatformMessageSummary,
|
||||
ProjectConfig,
|
||||
@@ -417,6 +418,12 @@ export const devopsApi = {
|
||||
return Array.isArray(data) ? data.map(sanitizeAuditLogEntry) : []
|
||||
},
|
||||
|
||||
async getNotificationOutbox(): Promise<NotificationOutboxMessage[]> {
|
||||
const { data } = await http.get<NotificationOutboxMessage[]>('/notification-outbox')
|
||||
lastDataSource = 'backend'
|
||||
return Array.isArray(data) ? data.map(sanitizeNotificationOutboxMessage) : []
|
||||
},
|
||||
|
||||
async getAgentCards(): Promise<AgentCard[]> {
|
||||
return agentCards
|
||||
},
|
||||
@@ -848,6 +855,25 @@ function sanitizeAuditLogEntry(record: AuditLogEntry): AuditLogEntry {
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeNotificationOutboxMessage(
|
||||
record: NotificationOutboxMessage,
|
||||
): NotificationOutboxMessage {
|
||||
return {
|
||||
id: safeText(record.id, 'outbox-unknown'),
|
||||
deployRunId: optionalSafeText(record.deployRunId),
|
||||
channel: safeText(record.channel, 'none'),
|
||||
template: safeText(record.template, 'unknown-template'),
|
||||
payloadSummary: safeRecord(record.payloadSummary),
|
||||
status: toNotificationOutboxStatus(record.status),
|
||||
attemptCount: Number.isFinite(record.attemptCount) ? record.attemptCount : 0,
|
||||
nextAttemptAt: optionalSafeText(record.nextAttemptAt),
|
||||
lastError: optionalSafeText(record.lastError),
|
||||
idempotencyKey: safeText(record.idempotencyKey, 'unknown-idempotency'),
|
||||
createdAt: safeText(record.createdAt, ''),
|
||||
updatedAt: safeText(record.updatedAt, ''),
|
||||
}
|
||||
}
|
||||
|
||||
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
|
||||
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
||||
const name = step.name || bpmnNodeId
|
||||
@@ -916,6 +942,21 @@ function toStepStatus(status?: BackendRunStatus): StepStatus {
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
function toNotificationOutboxStatus(
|
||||
status?: NotificationOutboxMessage['status'],
|
||||
): NotificationOutboxMessage['status'] {
|
||||
if (
|
||||
status === 'pending' ||
|
||||
status === 'sent' ||
|
||||
status === 'failed' ||
|
||||
status === 'dead'
|
||||
) {
|
||||
return status
|
||||
}
|
||||
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
function stepOwner(nodeId: string): PipelineStep['owner'] {
|
||||
if (nodeId.includes('gitea')) {
|
||||
return 'Gitea'
|
||||
|
||||
+19
-1
@@ -14,6 +14,7 @@ import type {
|
||||
ProjectConfig,
|
||||
ProjectRefs,
|
||||
JenkinsSyncSummary,
|
||||
NotificationOutboxMessage,
|
||||
ReleaseRun,
|
||||
SystemSetting,
|
||||
} from '../types/devops'
|
||||
@@ -30,6 +31,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
const agentCards = ref<AgentCard[]>([])
|
||||
const settings = ref<SystemSetting[]>([])
|
||||
const auditLogs = ref<AuditLogEntry[]>([])
|
||||
const notificationOutbox = ref<NotificationOutboxMessage[]>([])
|
||||
const integrationConfig = ref<IntegrationConfigStatus>({
|
||||
checkedAt: '',
|
||||
integrations: [],
|
||||
@@ -59,9 +61,17 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
const auditLogsPromise = includeAdminData
|
||||
? devopsApi.getAuditLogs()
|
||||
: Promise.resolve<AuditLogEntry[]>([])
|
||||
const [processXml, recentAuditLogs] = await Promise.all([processXmlPromise, auditLogsPromise])
|
||||
const notificationOutboxPromise = includeAdminData
|
||||
? devopsApi.getNotificationOutbox()
|
||||
: Promise.resolve<NotificationOutboxMessage[]>([])
|
||||
const [processXml, recentAuditLogs, recentNotificationOutbox] = await Promise.all([
|
||||
processXmlPromise,
|
||||
auditLogsPromise,
|
||||
notificationOutboxPromise,
|
||||
])
|
||||
releaseProcessXml.value = processXml
|
||||
auditLogs.value = recentAuditLogs
|
||||
notificationOutbox.value = recentNotificationOutbox
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -119,6 +129,12 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
return auditLogs.value
|
||||
}
|
||||
|
||||
async function loadNotificationOutbox() {
|
||||
notificationOutbox.value = await devopsApi.getNotificationOutbox()
|
||||
dataSource.value = devopsApi.getLastDataSource()
|
||||
return notificationOutbox.value
|
||||
}
|
||||
|
||||
function upsertRun(run: ReleaseRun) {
|
||||
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
|
||||
}
|
||||
@@ -132,6 +148,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
agentCards,
|
||||
settings,
|
||||
auditLogs,
|
||||
notificationOutbox,
|
||||
integrationConfig,
|
||||
releaseProcessXml,
|
||||
projectRefs,
|
||||
@@ -145,5 +162,6 @@ export const usePlatformStore = defineStore('platform', () => {
|
||||
syncJenkinsRuns,
|
||||
loadProjectRefs,
|
||||
loadAuditLogs,
|
||||
loadNotificationOutbox,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -317,6 +317,23 @@ export interface AuditLogEntry {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type NotificationOutboxStatus = 'pending' | 'sent' | 'failed' | 'dead'
|
||||
|
||||
export interface NotificationOutboxMessage {
|
||||
id: string
|
||||
deployRunId?: string
|
||||
channel: string
|
||||
template: string
|
||||
payloadSummary?: Record<string, unknown>
|
||||
status: NotificationOutboxStatus
|
||||
attemptCount: number
|
||||
nextAttemptAt?: string
|
||||
lastError?: string
|
||||
idempotencyKey: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface AgentCard {
|
||||
mode: AgentMode
|
||||
title: string
|
||||
|
||||
@@ -9,14 +9,17 @@ import AuditJsonCell from '../components/AuditJsonCell.vue'
|
||||
import StatusPill from '../components/StatusPill.vue'
|
||||
import { devopsApi } from '../services/devopsApi'
|
||||
import { usePlatformStore } from '../stores/platform'
|
||||
import { useSessionStore } from '../stores/session'
|
||||
import type {
|
||||
AgentConfigSummary,
|
||||
AuditLogEntry,
|
||||
IntegrationConfigItem,
|
||||
IntegrationConfigState,
|
||||
NotificationOutboxMessage,
|
||||
} from '../types/devops'
|
||||
|
||||
const platform = usePlatformStore()
|
||||
const session = useSessionStore()
|
||||
const agentConfigLoading = ref(false)
|
||||
const agentConfigTesting = ref(false)
|
||||
const agentConfig = ref<AgentConfigSummary | null>(null)
|
||||
@@ -42,6 +45,15 @@ const configStatusMeta: Record<
|
||||
partial: { label: '部分配置', type: 'warning' },
|
||||
missing: { label: '未配置', type: 'danger' },
|
||||
}
|
||||
const outboxStatusMeta: Record<
|
||||
NotificationOutboxMessage['status'],
|
||||
{ label: string; type: 'success' | 'warning' | 'danger' | 'info' }
|
||||
> = {
|
||||
pending: { label: '待投递', type: 'warning' },
|
||||
sent: { label: '已投递', type: 'success' },
|
||||
failed: { label: '投递失败', type: 'danger' },
|
||||
dead: { label: '已终止', type: 'info' },
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadAgentConfig()
|
||||
@@ -109,6 +121,20 @@ function auditPayload(record: AuditLogEntry): unknown {
|
||||
return Object.keys(payload).length > 0 ? payload : null
|
||||
}
|
||||
|
||||
function outboxPayload(record: NotificationOutboxMessage): unknown {
|
||||
return record.payloadSummary ?? null
|
||||
}
|
||||
|
||||
function outboxStatusLabel(status: NotificationOutboxMessage['status']): string {
|
||||
return outboxStatusMeta[status]?.label ?? '未知'
|
||||
}
|
||||
|
||||
function outboxStatusType(
|
||||
status: NotificationOutboxMessage['status'],
|
||||
): 'success' | 'warning' | 'danger' | 'info' {
|
||||
return outboxStatusMeta[status]?.type ?? 'info'
|
||||
}
|
||||
|
||||
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
|
||||
return [
|
||||
{
|
||||
@@ -211,6 +237,15 @@ async function testAgentConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshNotificationOutbox() {
|
||||
try {
|
||||
await platform.loadNotificationOutbox()
|
||||
ElMessage.success('通知 outbox 已刷新')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '通知 outbox 刷新失败')
|
||||
}
|
||||
}
|
||||
|
||||
function applyAgentConfig(config: AgentConfigSummary) {
|
||||
agentConfig.value = config
|
||||
agentConfigForm.baseURL = config.baseURL
|
||||
@@ -386,6 +421,58 @@ function applyAgentConfig(config: AgentConfigSummary) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="session.role === 'super_admin'" class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>通知 outbox</h2>
|
||||
<ElButton :icon="Refresh" size="small" @click="refreshNotificationOutbox">
|
||||
刷新
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ElTable :data="platform.notificationOutbox" border>
|
||||
<ElTableColumn label="创建时间" min-width="150">
|
||||
<template #default="{ row }">
|
||||
{{ formatCheckedAt(row.createdAt) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="outboxStatusType(row.status)" effect="light" round>
|
||||
{{ outboxStatusLabel(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="channel" label="渠道" min-width="100" />
|
||||
<ElTableColumn prop="template" label="模板" min-width="130" />
|
||||
<ElTableColumn label="发布单" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span class="mono">{{ row.deployRunId || '-' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="尝试" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.attemptCount }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="错误摘要" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<span class="outbox-error">{{ row.lastError || '-' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="脱敏 payload" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<AuditJsonCell :value="outboxPayload(row)" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<ElEmpty
|
||||
v-if="platform.notificationOutbox.length === 0"
|
||||
description="暂无通知 outbox 记录"
|
||||
:image-size="72"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>最近运维审计</h2>
|
||||
@@ -609,6 +696,12 @@ function applyAgentConfig(config: AgentConfigSummary) {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.outbox-error {
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.variable-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user