feat: 展示通知 outbox 状态
- services: 增加 /notification-outbox 真实接口读取与摘要清洗 - store: 超级管理员加载最近通知 outbox - settings: 增加通知 outbox 运维表格
This commit is contained in:
@@ -25,6 +25,7 @@ import type {
|
|||||||
LoginPayload,
|
LoginPayload,
|
||||||
LoginResult,
|
LoginResult,
|
||||||
MemberSummary,
|
MemberSummary,
|
||||||
|
NotificationOutboxMessage,
|
||||||
PipelineStep,
|
PipelineStep,
|
||||||
PlatformMessageSummary,
|
PlatformMessageSummary,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
@@ -417,6 +418,12 @@ export const devopsApi = {
|
|||||||
return Array.isArray(data) ? data.map(sanitizeAuditLogEntry) : []
|
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[]> {
|
async getAgentCards(): Promise<AgentCard[]> {
|
||||||
return agentCards
|
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 {
|
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
|
||||||
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
|
||||||
const name = step.name || bpmnNodeId
|
const name = step.name || bpmnNodeId
|
||||||
@@ -916,6 +942,21 @@ function toStepStatus(status?: BackendRunStatus): StepStatus {
|
|||||||
return 'pending'
|
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'] {
|
function stepOwner(nodeId: string): PipelineStep['owner'] {
|
||||||
if (nodeId.includes('gitea')) {
|
if (nodeId.includes('gitea')) {
|
||||||
return 'Gitea'
|
return 'Gitea'
|
||||||
|
|||||||
+19
-1
@@ -14,6 +14,7 @@ import type {
|
|||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
ProjectRefs,
|
ProjectRefs,
|
||||||
JenkinsSyncSummary,
|
JenkinsSyncSummary,
|
||||||
|
NotificationOutboxMessage,
|
||||||
ReleaseRun,
|
ReleaseRun,
|
||||||
SystemSetting,
|
SystemSetting,
|
||||||
} from '../types/devops'
|
} from '../types/devops'
|
||||||
@@ -30,6 +31,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
const agentCards = ref<AgentCard[]>([])
|
const agentCards = ref<AgentCard[]>([])
|
||||||
const settings = ref<SystemSetting[]>([])
|
const settings = ref<SystemSetting[]>([])
|
||||||
const auditLogs = ref<AuditLogEntry[]>([])
|
const auditLogs = ref<AuditLogEntry[]>([])
|
||||||
|
const notificationOutbox = ref<NotificationOutboxMessage[]>([])
|
||||||
const integrationConfig = ref<IntegrationConfigStatus>({
|
const integrationConfig = ref<IntegrationConfigStatus>({
|
||||||
checkedAt: '',
|
checkedAt: '',
|
||||||
integrations: [],
|
integrations: [],
|
||||||
@@ -59,9 +61,17 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
const auditLogsPromise = includeAdminData
|
const auditLogsPromise = includeAdminData
|
||||||
? devopsApi.getAuditLogs()
|
? devopsApi.getAuditLogs()
|
||||||
: Promise.resolve<AuditLogEntry[]>([])
|
: 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
|
releaseProcessXml.value = processXml
|
||||||
auditLogs.value = recentAuditLogs
|
auditLogs.value = recentAuditLogs
|
||||||
|
notificationOutbox.value = recentNotificationOutbox
|
||||||
dataSource.value = devopsApi.getLastDataSource()
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -119,6 +129,12 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
return auditLogs.value
|
return auditLogs.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadNotificationOutbox() {
|
||||||
|
notificationOutbox.value = await devopsApi.getNotificationOutbox()
|
||||||
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
|
return notificationOutbox.value
|
||||||
|
}
|
||||||
|
|
||||||
function upsertRun(run: ReleaseRun) {
|
function upsertRun(run: ReleaseRun) {
|
||||||
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
|
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
|
||||||
}
|
}
|
||||||
@@ -132,6 +148,7 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
agentCards,
|
agentCards,
|
||||||
settings,
|
settings,
|
||||||
auditLogs,
|
auditLogs,
|
||||||
|
notificationOutbox,
|
||||||
integrationConfig,
|
integrationConfig,
|
||||||
releaseProcessXml,
|
releaseProcessXml,
|
||||||
projectRefs,
|
projectRefs,
|
||||||
@@ -145,5 +162,6 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
syncJenkinsRuns,
|
syncJenkinsRuns,
|
||||||
loadProjectRefs,
|
loadProjectRefs,
|
||||||
loadAuditLogs,
|
loadAuditLogs,
|
||||||
|
loadNotificationOutbox,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -317,6 +317,23 @@ export interface AuditLogEntry {
|
|||||||
createdAt: string
|
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 {
|
export interface AgentCard {
|
||||||
mode: AgentMode
|
mode: AgentMode
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ import AuditJsonCell from '../components/AuditJsonCell.vue'
|
|||||||
import StatusPill from '../components/StatusPill.vue'
|
import StatusPill from '../components/StatusPill.vue'
|
||||||
import { devopsApi } from '../services/devopsApi'
|
import { devopsApi } from '../services/devopsApi'
|
||||||
import { usePlatformStore } from '../stores/platform'
|
import { usePlatformStore } from '../stores/platform'
|
||||||
|
import { useSessionStore } from '../stores/session'
|
||||||
import type {
|
import type {
|
||||||
AgentConfigSummary,
|
AgentConfigSummary,
|
||||||
AuditLogEntry,
|
AuditLogEntry,
|
||||||
IntegrationConfigItem,
|
IntegrationConfigItem,
|
||||||
IntegrationConfigState,
|
IntegrationConfigState,
|
||||||
|
NotificationOutboxMessage,
|
||||||
} from '../types/devops'
|
} from '../types/devops'
|
||||||
|
|
||||||
const platform = usePlatformStore()
|
const platform = usePlatformStore()
|
||||||
|
const session = useSessionStore()
|
||||||
const agentConfigLoading = ref(false)
|
const agentConfigLoading = ref(false)
|
||||||
const agentConfigTesting = ref(false)
|
const agentConfigTesting = ref(false)
|
||||||
const agentConfig = ref<AgentConfigSummary | null>(null)
|
const agentConfig = ref<AgentConfigSummary | null>(null)
|
||||||
@@ -42,6 +45,15 @@ const configStatusMeta: Record<
|
|||||||
partial: { label: '部分配置', type: 'warning' },
|
partial: { label: '部分配置', type: 'warning' },
|
||||||
missing: { label: '未配置', type: 'danger' },
|
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(() => {
|
onMounted(() => {
|
||||||
void loadAgentConfig()
|
void loadAgentConfig()
|
||||||
@@ -109,6 +121,20 @@ function auditPayload(record: AuditLogEntry): unknown {
|
|||||||
return Object.keys(payload).length > 0 ? payload : null
|
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[] {
|
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
|
||||||
return [
|
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) {
|
function applyAgentConfig(config: AgentConfigSummary) {
|
||||||
agentConfig.value = config
|
agentConfig.value = config
|
||||||
agentConfigForm.baseURL = config.baseURL
|
agentConfigForm.baseURL = config.baseURL
|
||||||
@@ -386,6 +421,58 @@ function applyAgentConfig(config: AgentConfigSummary) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>最近运维审计</h2>
|
<h2>最近运维审计</h2>
|
||||||
@@ -609,6 +696,12 @@ function applyAgentConfig(config: AgentConfigSummary) {
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.outbox-error {
|
||||||
|
color: #b42318;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
.variable-grid {
|
.variable-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user