feat: 展示项目 Jenkins 诊断状态
This commit is contained in:
@@ -29,6 +29,8 @@ import type {
|
|||||||
PipelineStep,
|
PipelineStep,
|
||||||
PlatformMessageSummary,
|
PlatformMessageSummary,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
|
ProjectJenkinsJobDiagnostic,
|
||||||
ProjectRefs,
|
ProjectRefs,
|
||||||
ReleaseRun,
|
ReleaseRun,
|
||||||
ReleaseStatus,
|
ReleaseStatus,
|
||||||
@@ -349,6 +351,14 @@ export const devopsApi = {
|
|||||||
return toProjectConfigs(await fetchProjects())
|
return toProjectConfigs(await fetchProjects())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getProjectJenkinsDiagnostics(): Promise<ProjectJenkinsDiagnosticsSummary> {
|
||||||
|
const { data } = await http.get<ProjectJenkinsDiagnosticsSummary>(
|
||||||
|
'/projects/jenkins-diagnostics',
|
||||||
|
)
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
return sanitizeProjectJenkinsDiagnostics(data)
|
||||||
|
},
|
||||||
|
|
||||||
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
|
async getProjectRefs(projectKey: string): Promise<ProjectRefs> {
|
||||||
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
|
const { data } = await http.get<ProjectRefs>(`/projects/${projectKey}/refs`)
|
||||||
lastDataSource = 'backend'
|
lastDataSource = 'backend'
|
||||||
@@ -787,6 +797,8 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
|||||||
lastReleasedAt: '-',
|
lastReleasedAt: '-',
|
||||||
lastActor: '-',
|
lastActor: '-',
|
||||||
jenkinsJob: environment.jenkinsJobPath,
|
jenkinsJob: environment.jenkinsJobPath,
|
||||||
|
jenkinsStatus: 'warning' as const,
|
||||||
|
jenkinsMessage: '尚未执行 Jenkins 诊断',
|
||||||
wecomRobot: '后端通知配置',
|
wecomRobot: '后端通知配置',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -800,6 +812,59 @@ function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeProjectJenkinsDiagnostics(
|
||||||
|
value: ProjectJenkinsDiagnosticsSummary,
|
||||||
|
): ProjectJenkinsDiagnosticsSummary {
|
||||||
|
return {
|
||||||
|
checkedAt: safeText(value.checkedAt, new Date().toISOString()),
|
||||||
|
jobs: Array.isArray(value.jobs)
|
||||||
|
? value.jobs.map(sanitizeProjectJenkinsJobDiagnostic)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeProjectJenkinsJobDiagnostic(
|
||||||
|
value: ProjectJenkinsJobDiagnostic,
|
||||||
|
): ProjectJenkinsJobDiagnostic {
|
||||||
|
return {
|
||||||
|
projectKey: safeText(value.projectKey, 'unknown-project'),
|
||||||
|
environment: toEnvironmentName(value.environment),
|
||||||
|
jenkinsJobPath: safeText(value.jenkinsJobPath, ''),
|
||||||
|
status: toProjectJenkinsDiagnosticStatus(value.status),
|
||||||
|
message: optionalSafeText(value.message),
|
||||||
|
buildable: typeof value.buildable === 'boolean' ? value.buildable : undefined,
|
||||||
|
url: optionalSafeText(value.url),
|
||||||
|
lastBuild: value.lastBuild
|
||||||
|
? {
|
||||||
|
number: Number.isFinite(value.lastBuild.number)
|
||||||
|
? value.lastBuild.number
|
||||||
|
: undefined,
|
||||||
|
result: value.lastBuild.result,
|
||||||
|
building:
|
||||||
|
typeof value.lastBuild.building === 'boolean'
|
||||||
|
? value.lastBuild.building
|
||||||
|
: undefined,
|
||||||
|
url: optionalSafeText(value.lastBuild.url),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectJenkinsDiagnosticStatus(
|
||||||
|
value: ProjectJenkinsJobDiagnostic['status'],
|
||||||
|
): ProjectJenkinsJobDiagnostic['status'] {
|
||||||
|
if (
|
||||||
|
value === 'ok' ||
|
||||||
|
value === 'missing' ||
|
||||||
|
value === 'not_configured' ||
|
||||||
|
value === 'unavailable'
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unavailable'
|
||||||
|
}
|
||||||
|
|
||||||
function toReleaseRun(run: BackendDeployRun, projectList: ProjectConfig[]): ReleaseRun {
|
function toReleaseRun(run: BackendDeployRun, projectList: ProjectConfig[]): ReleaseRun {
|
||||||
const projectKey = run.projectKey || 'unknown-project'
|
const projectKey = run.projectKey || 'unknown-project'
|
||||||
const project = projectList.find((item) => item.key === projectKey)
|
const project = projectList.find((item) => item.key === projectKey)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
IntegrationStatus,
|
IntegrationStatus,
|
||||||
IntegrationConfigStatus,
|
IntegrationConfigStatus,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
|
ProjectJenkinsDiagnosticsSummary,
|
||||||
ProjectRefs,
|
ProjectRefs,
|
||||||
JenkinsSyncSummary,
|
JenkinsSyncSummary,
|
||||||
NotificationOutboxMessage,
|
NotificationOutboxMessage,
|
||||||
@@ -123,6 +124,45 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
return refs
|
return refs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadProjectJenkinsDiagnostics(): Promise<ProjectJenkinsDiagnosticsSummary> {
|
||||||
|
const diagnostics = await devopsApi.getProjectJenkinsDiagnostics()
|
||||||
|
const jobByEnvironment = new Map(
|
||||||
|
diagnostics.jobs.map((job) => [`${job.projectKey}:${job.environment}`, job]),
|
||||||
|
)
|
||||||
|
|
||||||
|
projects.value = projects.value.map((project) => {
|
||||||
|
const environments = project.environments.map((environment) => {
|
||||||
|
const diagnostic = jobByEnvironment.get(`${project.key}:${environment.name}`)
|
||||||
|
|
||||||
|
if (!diagnostic) {
|
||||||
|
return environment
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...environment,
|
||||||
|
jenkinsJob: diagnostic.jenkinsJobPath || environment.jenkinsJob,
|
||||||
|
jenkinsStatus: jenkinsDiagnosticState(diagnostic.status),
|
||||||
|
jenkinsMessage: diagnostic.message || jenkinsDiagnosticMessage(diagnostic.status),
|
||||||
|
jenkinsUrl: diagnostic.url,
|
||||||
|
jenkinsLastBuild: diagnostic.lastBuild,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const hasError = environments.some((environment) => environment.jenkinsStatus === 'error')
|
||||||
|
const hasWarning = environments.some((environment) => environment.jenkinsStatus === 'warning')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
environments,
|
||||||
|
integrations: {
|
||||||
|
...project.integrations,
|
||||||
|
jenkins: hasError ? 'error' : hasWarning ? 'warning' : 'healthy',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAuditLogs() {
|
async function loadAuditLogs() {
|
||||||
auditLogs.value = await devopsApi.getAuditLogs()
|
auditLogs.value = await devopsApi.getAuditLogs()
|
||||||
dataSource.value = devopsApi.getLastDataSource()
|
dataSource.value = devopsApi.getLastDataSource()
|
||||||
@@ -171,8 +211,41 @@ export const usePlatformStore = defineStore('platform', () => {
|
|||||||
syncJenkinsRun,
|
syncJenkinsRun,
|
||||||
syncJenkinsRuns,
|
syncJenkinsRuns,
|
||||||
loadProjectRefs,
|
loadProjectRefs,
|
||||||
|
loadProjectJenkinsDiagnostics,
|
||||||
loadAuditLogs,
|
loadAuditLogs,
|
||||||
loadNotificationOutbox,
|
loadNotificationOutbox,
|
||||||
retryNotificationOutboxMessage,
|
retryNotificationOutboxMessage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function jenkinsDiagnosticState(
|
||||||
|
status: ProjectJenkinsDiagnosticsSummary['jobs'][number]['status'],
|
||||||
|
) {
|
||||||
|
if (status === 'ok') {
|
||||||
|
return 'healthy' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'not_configured') {
|
||||||
|
return 'warning' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'error' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
function jenkinsDiagnosticMessage(
|
||||||
|
status: ProjectJenkinsDiagnosticsSummary['jobs'][number]['status'],
|
||||||
|
): string {
|
||||||
|
if (status === 'ok') {
|
||||||
|
return 'Jenkins job 可达'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'missing') {
|
||||||
|
return 'Jenkins job 不存在'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'not_configured') {
|
||||||
|
return 'Jenkins 集成未配置'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Jenkins job 不可用'
|
||||||
|
}
|
||||||
|
|||||||
@@ -187,6 +187,15 @@ export interface EnvironmentStatus {
|
|||||||
lastReleasedAt: string
|
lastReleasedAt: string
|
||||||
lastActor: string
|
lastActor: string
|
||||||
jenkinsJob: string
|
jenkinsJob: string
|
||||||
|
jenkinsStatus: HealthState
|
||||||
|
jenkinsMessage?: string
|
||||||
|
jenkinsUrl?: string
|
||||||
|
jenkinsLastBuild?: {
|
||||||
|
number?: number
|
||||||
|
result?: 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'UNSTABLE'
|
||||||
|
building?: boolean
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
wecomRobot: string
|
wecomRobot: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +223,27 @@ export interface ProjectRefs {
|
|||||||
tags: ProjectRefItem[]
|
tags: ProjectRefItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectJenkinsJobDiagnostic {
|
||||||
|
projectKey: string
|
||||||
|
environment: EnvironmentName
|
||||||
|
jenkinsJobPath: string
|
||||||
|
status: 'ok' | 'missing' | 'not_configured' | 'unavailable'
|
||||||
|
message?: string
|
||||||
|
buildable?: boolean
|
||||||
|
url?: string
|
||||||
|
lastBuild?: {
|
||||||
|
number?: number
|
||||||
|
result?: 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'UNSTABLE'
|
||||||
|
building?: boolean
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectJenkinsDiagnosticsSummary {
|
||||||
|
checkedAt: string
|
||||||
|
jobs: ProjectJenkinsJobDiagnostic[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectRefCommit {
|
export interface ProjectRefCommit {
|
||||||
sha: string
|
sha: string
|
||||||
message: string
|
message: string
|
||||||
|
|||||||
@@ -1,8 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 项目配置页展示项目仓库、发布环境以及真实 Jenkins job 诊断状态。
|
||||||
|
*/
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
import StatusPill from '../components/StatusPill.vue'
|
import StatusPill from '../components/StatusPill.vue'
|
||||||
import { usePlatformStore } from '../stores/platform'
|
import { usePlatformStore } from '../stores/platform'
|
||||||
|
import type { EnvironmentStatus } from '../types/devops'
|
||||||
|
|
||||||
const platform = usePlatformStore()
|
const platform = usePlatformStore()
|
||||||
|
const diagnosticsLoading = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshJenkinsDiagnostics()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function refreshJenkinsDiagnostics() {
|
||||||
|
diagnosticsLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await platform.loadProjectJenkinsDiagnostics()
|
||||||
|
} finally {
|
||||||
|
diagnosticsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastBuildText(env: EnvironmentStatus): string {
|
||||||
|
if (!env.jenkinsLastBuild?.number) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.jenkinsLastBuild.building) {
|
||||||
|
return `#${env.jenkinsLastBuild.number} 运行中`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `#${env.jenkinsLastBuild.number} ${env.jenkinsLastBuild.result || 'UNKNOWN'}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,7 +50,14 @@ const platform = usePlatformStore()
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>项目清单</h2>
|
<h2>项目清单</h2>
|
||||||
<span>由后端 projects 接口返回</span>
|
<ElButton
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
:loading="diagnosticsLoading"
|
||||||
|
@click="refreshJenkinsDiagnostics"
|
||||||
|
>
|
||||||
|
刷新 Jenkins 诊断
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<ElTable :data="platform.projects" row-key="key">
|
<ElTable :data="platform.projects" row-key="key">
|
||||||
@@ -35,6 +74,23 @@ const platform = usePlatformStore()
|
|||||||
<dd>{{ env.branchPolicy }}</dd>
|
<dd>{{ env.branchPolicy }}</dd>
|
||||||
<dt>Jenkins Job</dt>
|
<dt>Jenkins Job</dt>
|
||||||
<dd class="mono">{{ env.jenkinsJob }}</dd>
|
<dd class="mono">{{ env.jenkinsJob }}</dd>
|
||||||
|
<dt>Job 状态</dt>
|
||||||
|
<dd>
|
||||||
|
<StatusPill :state="env.jenkinsStatus" />
|
||||||
|
<span class="diagnostic-message">{{ env.jenkinsMessage }}</span>
|
||||||
|
</dd>
|
||||||
|
<dt>最近构建</dt>
|
||||||
|
<dd>
|
||||||
|
<a
|
||||||
|
v-if="env.jenkinsLastBuild?.url"
|
||||||
|
:href="env.jenkinsLastBuild.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{{ lastBuildText(env) }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ lastBuildText(env) }}</span>
|
||||||
|
</dd>
|
||||||
<dt>通知通道</dt>
|
<dt>通知通道</dt>
|
||||||
<dd>{{ env.wecomRobot }}</dd>
|
<dd>{{ env.wecomRobot }}</dd>
|
||||||
<dt>最近发布</dt>
|
<dt>最近发布</dt>
|
||||||
@@ -116,6 +172,12 @@ dd {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagnostic-message {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #596578;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.env-config {
|
.env-config {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user