feat: 展示 Jenkins 构建日志
This commit is contained in:
@@ -1,12 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
/**
|
||||||
import type { ReleaseRun } from '../types/devops'
|
* 发布执行详情聚合 run 元数据、步骤摘要和按需读取的 Jenkins build 日志。
|
||||||
|
*/
|
||||||
|
import { RefreshRight, Tickets } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { devopsApi } from '../services/devopsApi'
|
||||||
|
import type { JenkinsBuildLog, ReleaseRun } from '../types/devops'
|
||||||
import StatusPill from './StatusPill.vue'
|
import StatusPill from './StatusPill.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
run: ReleaseRun
|
run: ReleaseRun
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const logVisible = ref(false)
|
||||||
|
const logLoading = ref(false)
|
||||||
|
const logText = ref('')
|
||||||
|
const logMeta = ref<JenkinsBuildLog | null>(null)
|
||||||
|
|
||||||
const notableSteps = computed(() =>
|
const notableSteps = computed(() =>
|
||||||
props.run.steps.filter(
|
props.run.steps.filter(
|
||||||
(step) =>
|
(step) =>
|
||||||
@@ -18,6 +29,16 @@ const notableSteps = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const canReadJenkinsLog = computed(
|
||||||
|
() => props.run.jenkinsBuildNumber !== undefined && props.run.jenkinsBuildNumber !== null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const logTitle = computed(() => {
|
||||||
|
const buildNumber = props.run.jenkinsBuildNumber
|
||||||
|
|
||||||
|
return buildNumber ? `Jenkins 日志 #${buildNumber}` : 'Jenkins 日志'
|
||||||
|
})
|
||||||
|
|
||||||
function noteLabel(status: string) {
|
function noteLabel(status: string) {
|
||||||
return status === 'failed' ? '失败摘要' : '执行提示'
|
return status === 'failed' ? '失败摘要' : '执行提示'
|
||||||
}
|
}
|
||||||
@@ -49,6 +70,47 @@ function buildText() {
|
|||||||
|
|
||||||
return '未返回'
|
return '未返回'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetJenkinsLog() {
|
||||||
|
logVisible.value = false
|
||||||
|
logLoading.value = false
|
||||||
|
logText.value = ''
|
||||||
|
logMeta.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openJenkinsLog() {
|
||||||
|
if (!canReadJenkinsLog.value) {
|
||||||
|
ElMessage.warning('当前发布单尚未关联 Jenkins build')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logVisible.value = true
|
||||||
|
|
||||||
|
if (!logText.value) {
|
||||||
|
await loadJenkinsLog(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadJenkinsLog() {
|
||||||
|
await loadJenkinsLog(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreJenkinsLog() {
|
||||||
|
await loadJenkinsLog(logMeta.value?.nextStart ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadJenkinsLog(start: number) {
|
||||||
|
logLoading.value = true
|
||||||
|
try {
|
||||||
|
const log = await devopsApi.getJenkinsLog(props.run.id, start)
|
||||||
|
logMeta.value = log
|
||||||
|
logText.value = start === 0 ? log.text : `${logText.value}${log.text}`
|
||||||
|
} finally {
|
||||||
|
logLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.run.id, resetJenkinsLog)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -74,6 +136,18 @@ function buildText() {
|
|||||||
<dd class="mono">{{ displayText(run.jenkinsQueueId) }}</dd>
|
<dd class="mono">{{ displayText(run.jenkinsQueueId) }}</dd>
|
||||||
<dt>Jenkins Build</dt>
|
<dt>Jenkins Build</dt>
|
||||||
<dd class="mono">{{ buildText() }}</dd>
|
<dd class="mono">{{ buildText() }}</dd>
|
||||||
|
<dt>Jenkins 日志</dt>
|
||||||
|
<dd>
|
||||||
|
<ElButton
|
||||||
|
size="small"
|
||||||
|
:icon="Tickets"
|
||||||
|
:disabled="!canReadJenkinsLog"
|
||||||
|
:loading="logLoading"
|
||||||
|
@click="openJenkinsLog"
|
||||||
|
>
|
||||||
|
查看日志
|
||||||
|
</ElButton>
|
||||||
|
</dd>
|
||||||
<dt>Jenkins 状态</dt>
|
<dt>Jenkins 状态</dt>
|
||||||
<dd>{{ displayText(run.jenkinsSummary) }}</dd>
|
<dd>{{ displayText(run.jenkinsSummary) }}</dd>
|
||||||
<dt>Build URL</dt>
|
<dt>Build URL</dt>
|
||||||
@@ -122,6 +196,31 @@ function buildText() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-line">后端尚未返回异常摘要、执行提示或日志摘要。</div>
|
<div v-else class="empty-line">后端尚未返回异常摘要、执行提示或日志摘要。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ElDrawer v-model="logVisible" :title="logTitle" size="min(760px, 92vw)" append-to-body>
|
||||||
|
<div class="jenkins-log-drawer">
|
||||||
|
<div class="jenkins-log-toolbar">
|
||||||
|
<span>
|
||||||
|
{{ logMeta ? `${logMeta.jobPath} · start ${logMeta.start} -> ${logMeta.nextStart}` : '等待读取' }}
|
||||||
|
</span>
|
||||||
|
<div class="log-actions">
|
||||||
|
<ElButton size="small" :icon="RefreshRight" :loading="logLoading" @click="reloadJenkinsLog">
|
||||||
|
重新读取
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="logMeta?.hasMore"
|
||||||
|
size="small"
|
||||||
|
:loading="logLoading"
|
||||||
|
@click="loadMoreJenkinsLog"
|
||||||
|
>
|
||||||
|
继续加载
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre v-if="logText" class="jenkins-log-output">{{ logText }}</pre>
|
||||||
|
<div v-else class="empty-line">Jenkins 暂无日志。</div>
|
||||||
|
</div>
|
||||||
|
</ElDrawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -155,6 +254,10 @@ function buildText() {
|
|||||||
color: #1d5fd0;
|
color: #1d5fd0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-grid :deep(.el-button) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.failure-block {
|
.failure-block {
|
||||||
border-top: 1px solid #edf1f7;
|
border-top: 1px solid #edf1f7;
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
@@ -235,6 +338,44 @@ function buildText() {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jenkins-log-drawer {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jenkins-log-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
color: #5f6b7a;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jenkins-log-output {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 420px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #111827;
|
||||||
|
color: #e5edf8;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -244,5 +385,10 @@ function buildText() {
|
|||||||
.detail-grid dd + dt {
|
.detail-grid dd + dt {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jenkins-log-toolbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type {
|
|||||||
IntegrationConfigStatus,
|
IntegrationConfigStatus,
|
||||||
IntegrationVariableHelp,
|
IntegrationVariableHelp,
|
||||||
IntegrationStatus,
|
IntegrationStatus,
|
||||||
|
JenkinsBuildLog,
|
||||||
JenkinsSyncSummary,
|
JenkinsSyncSummary,
|
||||||
LoginPayload,
|
LoginPayload,
|
||||||
LoginResult,
|
LoginResult,
|
||||||
@@ -402,6 +403,14 @@ export const devopsApi = {
|
|||||||
return toJenkinsSyncSummary(data, projectList)
|
return toJenkinsSyncSummary(data, projectList)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getJenkinsLog(runId: string, start = 0): Promise<JenkinsBuildLog> {
|
||||||
|
const { data } = await http.get<JenkinsBuildLog>(`/deploy-runs/${runId}/jenkins-log`, {
|
||||||
|
params: { start },
|
||||||
|
})
|
||||||
|
lastDataSource = 'backend'
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
async getAuditLogs(): Promise<AuditLogEntry[]> {
|
async getAuditLogs(): Promise<AuditLogEntry[]> {
|
||||||
const { data } = await http.get<AuditLogEntry[]>('/audit-logs')
|
const { data } = await http.get<AuditLogEntry[]>('/audit-logs')
|
||||||
lastDataSource = 'backend'
|
lastDataSource = 'backend'
|
||||||
|
|||||||
@@ -277,6 +277,17 @@ export interface ReleaseRun {
|
|||||||
steps: PipelineStep[]
|
steps: PipelineStep[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JenkinsBuildLog {
|
||||||
|
runId: string
|
||||||
|
jobPath: string
|
||||||
|
buildNumber: number
|
||||||
|
start: number
|
||||||
|
nextStart: number
|
||||||
|
hasMore: boolean
|
||||||
|
text: string
|
||||||
|
fetchedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface JenkinsSyncError {
|
export interface JenkinsSyncError {
|
||||||
id: string
|
id: string
|
||||||
projectKey: string
|
projectKey: string
|
||||||
|
|||||||
Reference in New Issue
Block a user