enhance(actions): Make Summary UI more beautiful with more infos (#37824)

## Summary

- Redesign the Actions run summary header to follow GitHub Actions
layout: trigger info on the left, Status / Total duration / Artifacts
columns inline on the right
- Expose trigger user avatar, pull request link, and PR head branch info
from the run view API
- Update the workflow graph header to show the workflow filename (linked
to the run workflow file) and `on: <event>`, while keeping the
jobs/dependencies/success stats line
- Remove the redundant commit/workflow metadata row below the run title;
that information now lives in the summary bar

New:
<img width="1564" height="639"
src="https://github.com/user-attachments/assets/e6bc1623-c5fc-4e97-abc9-fde7f3c6aef9"
/>

Old:
<img width="2038" height="1038"
src="https://github.com/user-attachments/assets/0857f19a-8d3a-4da2-82fd-e9ebeb200062"
/>

Replaces https://github.com/go-gitea/gitea/pull/36721

---------

Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
bircni
2026-06-08 20:49:06 +02:00
committed by GitHub
parent e01af366e2
commit b1c088e9cf
12 changed files with 524 additions and 115 deletions
+185 -21
View File
@@ -1,5 +1,4 @@
<script setup lang="ts">
import ActionStatusIcon from './ActionStatusIcon.vue';
import WorkflowGraph from './WorkflowGraph.vue';
import type {ActionRunViewStore} from "./ActionRunView.ts";
import {computed, onBeforeUnmount, onMounted, toRefs} from "vue";
@@ -11,6 +10,7 @@ defineOptions({
const props = defineProps<{
store: ActionRunViewStore;
locale: Record<string, any>;
artifactCount: number;
}>();
const locale = props.locale;
@@ -25,12 +25,27 @@ const topLevelJobs = computed(() => (run.value.jobs || []).filter((j) => !j.pare
const triggerUser = computed(() => {
const currentAttempt = run.value.attempts.find((attempt) => attempt.current);
if (currentAttempt) {
return {name: currentAttempt.triggerUserName, link: currentAttempt.triggerUserLink};
return {
name: currentAttempt.triggerUserName,
link: currentAttempt.triggerUserLink,
avatar: currentAttempt.triggerUserAvatar,
};
}
const pusher = run.value.commit.pusher;
return pusher.displayName ? {name: pusher.displayName, link: pusher.link} : null;
return pusher.displayName ? {
name: pusher.displayName,
link: pusher.link,
avatar: pusher.avatarLink,
} : null;
});
const triggerLabel = computed(() => {
if (isRerun.value) return locale.rerunTriggered;
return locale.triggeredVia.replace('%s', run.value.triggerEvent);
});
const artifactsDisplay = computed(() => props.artifactCount > 0 ? String(props.artifactCount) : '');
onMounted(async () => {
await props.store.startPollingCurrentRun();
});
@@ -42,19 +57,60 @@ onBeforeUnmount(() => {
<template>
<div class="action-run-summary-view">
<div class="action-run-summary-block">
<div class="flex-text-block">
<span>{{ isRerun ? locale.rerun : locale.triggeredVia.replace('%s', run.triggerEvent) }}</span>
<template v-if="triggerUser">
<span></span>
<a v-if="triggerUser.link" class="muted" :href="triggerUser.link">{{ triggerUser.name }}</a>
<span v-else class="muted">{{ triggerUser.name }}</span>
</template>
<span></span>
<relative-time :datetime="run.triggeredAt || ''" prefix=""/>
<div class="action-run-summary-trigger">
<span class="action-run-summary-label">
{{ triggerLabel }} <relative-time :datetime="run.triggeredAt || ''" prefix=""/>
</span>
<div class="flex-text-block tw-flex-wrap action-run-summary-trigger-content">
<component
:is="triggerUser.link ? 'a' : 'span'"
v-if="triggerUser"
class="flex-text-inline action-run-summary-user"
:class="{silenced: triggerUser.link}"
:href="triggerUser.link || undefined"
>
<img
v-if="triggerUser.avatar"
class="ui avatar tw-align-middle"
:src="triggerUser.avatar"
width="16"
height="16"
:alt="triggerUser.name"
>
<span>{{ triggerUser.name }}</span>
</component>
<a v-if="run.pullRequest" class="action-run-summary-pr silenced" :href="run.pullRequest.link">{{ run.pullRequest.index }}</a>
<span v-else-if="run.commit.branch.name" class="action-run-summary-branch-label tw-max-w-full">
<a
v-if="!run.commit.branch.isDeleted && run.commit.branch.link"
class="gt-ellipsis silenced"
:href="run.commit.branch.link"
:title="run.commit.branch.name"
>{{ run.commit.branch.name }}</a>
<span
v-else
class="gt-ellipsis tw-line-through"
:title="run.commit.branch.name"
>{{ run.commit.branch.name }}</span>
</span>
</div>
</div>
<div class="flex-text-block">
<ActionStatusIcon :locale-status="locale.status[run.status]" :status="run.status" :size="16" icon-variant="circle-fill"/>
<span>{{ locale.status[run.status] }}</span> <span>{{ locale.totalDuration }} {{ run.duration || '' }}</span>
<div class="action-run-summary-stat-divider"/>
<div class="action-run-summary-stat">
<span class="action-run-summary-label">{{ locale.statusLabel }}</span>
<span class="action-run-summary-stat-value">{{ locale.status[run.status] }}</span>
</div>
<div class="action-run-summary-stat">
<span class="action-run-summary-label">{{ locale.totalDuration }}</span>
<span class="action-run-summary-stat-value">{{ run.duration || '' }}</span>
</div>
<div class="action-run-summary-stat action-run-summary-stat-last">
<span class="action-run-summary-label">{{ locale.artifactsTitle }}</span>
<span class="action-run-summary-stat-value">{{ artifactsDisplay }}</span>
</div>
</div>
<WorkflowGraph
@@ -63,6 +119,8 @@ onBeforeUnmount(() => {
:jobs="topLevelJobs"
:run-link="run.link"
:workflow-id="run.workflowID"
:workflow-link="`${run.link}/workflow`"
:trigger-event="run.triggerEvent"
:locale="locale"
/>
</div>
@@ -77,13 +135,119 @@ onBeforeUnmount(() => {
.action-run-summary-block {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 12px;
align-items: stretch; /* equal-height columns so labels align at top and values at bottom */
padding: 12px 16px;
border-bottom: 1px solid var(--color-secondary);
border-radius: var(--border-radius) var(--border-radius) 0 0;
background: var(--color-box-header);
background: var(--color-console-bg);
}
.action-run-summary-trigger {
display: flex;
flex-direction: column;
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
margin-right: 24px;
}
.action-run-summary-label {
display: block;
margin-bottom: 4px;
font-size: 12px;
line-height: 1.4;
color: var(--color-text-light-2);
}
.action-run-summary-trigger-content {
margin-top: auto; /* pin trigger content to the bottom, aligned with the stat values */
color: var(--color-text-light-2);
align-items: center;
}
.action-run-summary-user {
font-weight: var(--font-weight-semibold);
color: var(--color-text);
line-height: 16px;
}
.action-run-summary-user .ui.avatar {
margin: 0;
}
.action-run-summary-pr {
color: var(--color-text);
line-height: 16px;
}
.action-run-summary-branch-label {
display: inline-flex;
align-items: center;
max-width: 200px;
min-height: 20px;
padding: 0 6px;
border-radius: var(--border-radius);
background: var(--color-primary-light-6);
color: var(--color-primary);
font-size: 12px;
line-height: 20px;
font-family: var(--fonts-monospace);
}
.action-run-summary-branch-label a {
color: inherit;
}
.action-run-summary-branch-label a:hover {
text-decoration: underline;
}
.action-run-summary-user:hover span {
color: var(--color-primary);
}
.action-run-summary-stat {
display: flex;
flex-direction: column;
flex: 0 0 auto;
min-width: 72px;
margin-left: 24px;
margin-right: 24px;
}
.action-run-summary-stat-last {
margin-right: 0;
}
.action-run-summary-stat-divider {
display: none;
flex: 0 0 100%;
margin: 8px 0;
border-bottom: 1px solid var(--color-secondary);
}
.action-run-summary-stat-value {
display: block;
margin-top: auto; /* pin value to the bottom so all column values share a baseline */
font-size: 16px;
line-height: 1.25;
font-weight: var(--font-weight-semibold);
color: var(--color-text);
}
@media (max-width: 767.98px) {
.action-run-summary-trigger {
flex: 0 0 100%;
margin-right: 0;
}
.action-run-summary-stat {
margin-left: 0;
margin-right: 24px;
}
.action-run-summary-stat-divider {
display: block;
}
}
</style>
+2
View File
@@ -126,6 +126,7 @@ export function createEmptyActionsRun(): ActionsRun {
duration: '',
triggeredAt: 0,
triggerEvent: '',
pullRequest: null,
jobs: [] as Array<ActionsJob>,
commit: {
localeCommit: '',
@@ -135,6 +136,7 @@ export function createEmptyActionsRun(): ActionsRun {
pusher: {
displayName: '',
link: '',
avatarLink: '',
},
branch: {
name: '',
+37 -36
View File
@@ -85,6 +85,16 @@ function formatCurrentAttemptTitle(attempt: ActionsRunAttempt) {
return attempt.latest ? `${locale.latest} #${attempt.attempt}` : formatAttemptTitle(attempt);
}
const backLink = computed(() => {
if (run.value.pullRequest) {
return {href: run.value.pullRequest.link, prefix: locale.backToPullRequest, name: run.value.pullRequest.index};
}
if (run.value.workflowLink) {
return {href: run.value.workflowLink, prefix: locale.backToWorkflow, name: run.value.workflowID.replace(/\.(yml|yaml)$/i, '')};
}
return null;
});
function buildArtifactLink(name: string) {
const searchString = run.value.runAttempt > 0 ? `?attempt=${run.value.runAttempt}` : '';
return `${run.value.link}/artifacts/${encodeURIComponent(name)}${searchString}`;
@@ -108,9 +118,13 @@ async function deleteArtifact(name: string) {
<!-- make the view container full width to make users easier to read logs -->
<div class="ui fluid container">
<div class="action-view-header">
<a v-if="backLink" class="action-view-back silenced" :href="backLink.href">
<SvgIcon name="octicon-arrow-left" :size="14"/>
<span>{{ backLink.prefix }} <span class="action-view-back-name">{{ backLink.name }}</span></span>
</a>
<div class="action-info-summary">
<div class="action-info-summary-title">
<ActionStatusIcon :locale-status="locale.status[run.status]" :status="run.status" :size="20" icon-variant="circle-fill"/>
<ActionStatusIcon :locale-status="locale.status[run.status]" :status="run.status" :size="22" icon-variant="circle-fill"/>
<!-- eslint-disable-next-line vue/no-v-html -->
<h2 class="action-info-summary-title-text" v-html="run.titleHTML"/>
</div>
@@ -172,26 +186,6 @@ async function deleteArtifact(name: string) {
</div>
</div>
</div>
<div class="action-commit-summary">
<span>
<a v-if="run.workflowLink" class="muted" :href="run.workflowLink"><b>{{ run.workflowID }}</b></a>
<b v-else>{{ run.workflowID }}</b>
:
</span>
<template v-if="run.isSchedule">
{{ locale.scheduled }}
</template>
<template v-else>
{{ locale.commit }}
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
{{ locale.pushedBy }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
</template>
<span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
<span v-if="run.commit.branch.isDeleted" class="gt-ellipsis tw-line-through" :data-tooltip-content="run.commit.branch.name">{{ run.commit.branch.name }}</span>
<a v-else class="gt-ellipsis" :href="run.commit.branch.link" :data-tooltip-content="run.commit.branch.name">{{ run.commit.branch.name }}</a>
</span>
</div>
</div>
<div class="action-view-body">
<div class="action-view-left">
@@ -287,6 +281,7 @@ async function deleteArtifact(name: string) {
v-if="!props.jobId"
:store="store"
:locale="locale"
:artifact-count="artifacts.length"
/>
<ActionRunJobView
v-else
@@ -311,9 +306,30 @@ async function deleteArtifact(name: string) {
/* action view header */
.action-view-header {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
}
.action-view-back {
display: inline-flex;
align-items: center;
align-self: flex-start;
gap: 4px;
font-size: 13px;
color: var(--color-text-light-1);
}
.action-view-back:hover {
color: var(--color-primary);
}
.action-view-back-name {
font-weight: var(--font-weight-bold);
color: var(--color-text);
}
.action-info-summary {
display: flex;
flex-wrap: wrap;
@@ -340,21 +356,6 @@ async function deleteArtifact(name: string) {
white-space: nowrap;
}
.action-commit-summary {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 5px;
margin-left: 28px;
}
@media (max-width: 767.98px) {
.action-commit-summary {
margin-left: 0;
margin-top: 8px;
}
}
/* ================ */
/* action view left */
+30 -9
View File
@@ -30,6 +30,8 @@ const props = defineProps<{
jobs: ActionsJob[];
runLink: string;
workflowId: string;
workflowLink?: string;
triggerEvent?: string;
locale: Record<string, string>;
}>();
@@ -231,9 +233,13 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
<template>
<div v-if="jobs.length > 0" class="workflow-graph">
<div class="graph-header">
<h4 class="graph-title">{{ locale.workflowDependencies }}</h4>
<div class="graph-workflow-info">
<a v-if="workflowLink" class="graph-workflow-name silenced" :href="workflowLink">{{ workflowId }}</a>
<span v-else class="graph-workflow-name">{{ workflowId }}</span>
<div v-if="triggerEvent" class="graph-workflow-trigger">on: {{ triggerEvent }}</div>
</div>
<div class="graph-stats">{{ graphStats }}</div>
<div class="flex-text-block">
<div class="flex-text-block graph-controls">
<button
type="button"
@click="zoomIn"
@@ -424,20 +430,29 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 14px;
background: var(--color-box-header);
border-bottom: 1px solid var(--color-secondary);
padding: 16px 16px 8px;
background: var(--color-console-bg);
gap: var(--gap-block);
flex-wrap: wrap;
}
.graph-title {
margin: 0;
.graph-workflow-info {
min-width: 0;
}
.graph-workflow-name {
display: block;
color: var(--color-text);
font-size: 16px;
font-weight: var(--font-weight-semibold);
flex: 1;
min-width: 200px;
line-height: 1.25;
}
.graph-workflow-trigger {
margin-top: 4px;
color: var(--color-text-light-2);
font-size: 12px;
line-height: 1.4;
}
.graph-stats {
@@ -447,6 +462,12 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
color: var(--color-text-light-1);
font-size: 13px;
white-space: nowrap;
margin-left: auto;
padding: 0 16px;
}
.graph-controls {
flex-shrink: 0;
}
.graph-container {
+4
View File
@@ -30,6 +30,10 @@ export function initRepositoryActionView() {
expandCallerJobs: el.getAttribute('data-locale-expand-caller-jobs'),
collapseCallerJobs: el.getAttribute('data-locale-collapse-caller-jobs'),
triggeredVia: el.getAttribute('data-locale-triggered-via'),
rerunTriggered: el.getAttribute('data-locale-rerun-triggered'),
backToPullRequest: el.getAttribute('data-locale-back-to-pull-request'),
backToWorkflow: el.getAttribute('data-locale-back-to-workflow'),
statusLabel: el.getAttribute('data-locale-status-label'),
totalDuration: el.getAttribute('data-locale-total-duration'),
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
artifactExpired: el.getAttribute('data-locale-artifact-expired'),
+6
View File
@@ -23,6 +23,10 @@ export type ActionsRun = {
duration: string,
triggeredAt: number,
triggerEvent: string,
pullRequest?: {
index: string,
link: string,
} | null,
jobs: Array<ActionsJob>,
commit: {
localeCommit: string,
@@ -32,6 +36,7 @@ export type ActionsRun = {
pusher: {
displayName: string,
link: string,
avatarLink: string,
},
branch: {
name: string,
@@ -51,6 +56,7 @@ export type ActionsRunAttempt = {
triggeredAt: number;
triggerUserName: string;
triggerUserLink: string;
triggerUserAvatar: string;
};
export type ActionsJob = {
+2
View File
@@ -7,6 +7,7 @@ import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox
import giteaExclamation from '../../public/assets/img/svg/gitea-exclamation.svg';
import giteaRunning from '../../public/assets/img/svg/gitea-running.svg';
import octiconArchive from '../../public/assets/img/svg/octicon-archive.svg';
import octiconArrowLeft from '../../public/assets/img/svg/octicon-arrow-left.svg';
import octiconArrowSwitch from '../../public/assets/img/svg/octicon-arrow-switch.svg';
import octiconBlocked from '../../public/assets/img/svg/octicon-blocked.svg';
import octiconBold from '../../public/assets/img/svg/octicon-bold.svg';
@@ -94,6 +95,7 @@ const svgs = {
'gitea-exclamation': giteaExclamation,
'gitea-running': giteaRunning,
'octicon-archive': octiconArchive,
'octicon-arrow-left': octiconArrowLeft,
'octicon-arrow-switch': octiconArrowSwitch,
'octicon-blocked': octiconBlocked,
'octicon-bold': octiconBold,