mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-28 14:04:00 +00:00
enhance(actions): add pending job status and align job statuses with GitHub (#39376)
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
<!-- Keep in sync with templates/repo/icons/action_status.tmpl.
|
||||
action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, cancelling, unknown.
|
||||
-->
|
||||
<!-- Keep in sync with templates/repo/icons/action_status.tmpl. -->
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue';
|
||||
import SvgIcon from './SvgIcon.vue';
|
||||
import {getActionStatusIcon, type ActionStatusIconVariant} from '../modules/action-status-icon.ts';
|
||||
import type {ActionsStatus} from '../modules/gitea-actions.ts';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
status: 'success' | 'skipped' | 'waiting' | 'blocked' | 'running' | 'failure' | 'cancelled' | 'cancelling' | 'unknown',
|
||||
status: ActionsStatus,
|
||||
size?: number,
|
||||
className?: string,
|
||||
localeStatus?: string,
|
||||
|
||||
@@ -92,7 +92,7 @@ function isJobCollapsed(jobID: number) {
|
||||
}
|
||||
|
||||
const visibleJobListItems = computed<JobListItem[]>(() => {
|
||||
const jobs = [...(run.value.jobs || [])].sort((a, b) => a.id - b.id);
|
||||
const jobs = (run.value.jobs || []).filter((job) => job.status !== 'pending' || job.id === props.jobId).sort((a, b) => a.id - b.id);
|
||||
const childrenByParent = buildJobsByParentJobID(jobs);
|
||||
|
||||
const result: JobListItem[] = [];
|
||||
|
||||
@@ -160,7 +160,7 @@ test('reusable callers with identical dependency signature are kept as separate
|
||||
{id: 1, link: '', jobId: 'prepare', name: 'prepare', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '30s'},
|
||||
{id: 2, link: '', jobId: 'local_caller', name: 'local caller', status: 'running', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '5m', needs: ['prepare'], callUses: './.gitea/workflows/lib.yml'},
|
||||
{id: 3, link: '', jobId: 'cross_caller', name: 'cross-repo caller', status: 'waiting', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '0s', needs: ['prepare'], callUses: 'user2/lib/.gitea/workflows/ext.yml@main'},
|
||||
{id: 4, link: '', jobId: 'final', name: 'final', status: 'blocked', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']},
|
||||
{id: 4, link: '', jobId: 'final', name: 'final', status: 'pending', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']},
|
||||
];
|
||||
const graph = createWorkflowGraphModel(jobs);
|
||||
expect(graph.nodes.find((n) => n.type === 'group')).toBeUndefined();
|
||||
|
||||
@@ -122,9 +122,10 @@ function compareStatusWorstFirst(a: ActionsStatus, b: ActionsStatus): number {
|
||||
if (s === 'running') return 2;
|
||||
if (s === 'waiting') return 3;
|
||||
if (s === 'blocked') return 4;
|
||||
if (s === 'success') return 5;
|
||||
if (s === 'skipped') return 6;
|
||||
return 7;
|
||||
if (s === 'pending') return 5;
|
||||
if (s === 'success') return 6;
|
||||
if (s === 'skipped') return 7;
|
||||
return 8;
|
||||
};
|
||||
return rank(a) - rank(b);
|
||||
}
|
||||
|
||||
@@ -228,11 +228,13 @@ const splitRoutedEdges = computed(() => {
|
||||
const nodesWithIncomingEdge = computed(() => new Set(graphModel.value.adjacency.incomingByNodeId.keys()));
|
||||
const nodesWithOutgoingEdge = computed(() => new Set(graphModel.value.adjacency.outgoingByNodeId.keys()));
|
||||
|
||||
function isJobLinked(job: ActionsJob) {
|
||||
return !job.isReusableCaller && job.status !== 'pending'; // callers have no detail page, pending jobs have nothing to show yet
|
||||
}
|
||||
|
||||
function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
const target = 'jobs' in job ? job.jobs[0]! : job;
|
||||
// Reusable callers have no per-job detail page; clicking them is a no-op so the graph
|
||||
// doesn't lead users to a dead destination.
|
||||
if (target.isReusableCaller) return;
|
||||
if (!isJobLinked(target)) return;
|
||||
const link = `${props.runLink}/jobs/${target.id}`;
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(link, '_blank');
|
||||
@@ -342,6 +344,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
v-for="ch in job.jobs"
|
||||
:key="ch.id"
|
||||
class="graph-list-row"
|
||||
:class="{ 'unlinked-node': !isJobLinked(ch) }"
|
||||
@mouseenter="handleNodeMouseEnter(job.id)"
|
||||
@click.stop="onNodeClick(ch, $event)"
|
||||
>
|
||||
@@ -373,6 +376,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
v-for="ch in job.jobs"
|
||||
:key="ch.id"
|
||||
class="graph-list-row"
|
||||
:class="{ 'unlinked-node': !isJobLinked(ch) }"
|
||||
@mouseenter="handleNodeMouseEnter(job.id)"
|
||||
@click="onNodeClick(ch, $event)"
|
||||
>
|
||||
@@ -391,7 +395,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
<g
|
||||
v-else
|
||||
class="job-node-group"
|
||||
:class="{ 'related-node': isNodeHighlighted(job.id), 'caller-node': job.jobs[0]!.isReusableCaller }"
|
||||
:class="{ 'related-node': isNodeHighlighted(job.id), 'unlinked-node': !isJobLinked(job.jobs[0]!) }"
|
||||
@click="onNodeClick(job, $event)"
|
||||
@mouseenter="handleNodeMouseEnter(job.id)"
|
||||
@mouseleave="handleNodeMouseLeave"
|
||||
@@ -523,10 +527,15 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.job-node-group.caller-node {
|
||||
.job-node-group.unlinked-node,
|
||||
.graph-list-row.unlinked-node {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.graph-list-row.unlinked-node:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.job-node-group:hover .job-rect,
|
||||
.job-node-group.related-node .job-rect {
|
||||
stroke: var(--color-primary);
|
||||
|
||||
@@ -89,6 +89,7 @@ function initRepositoryActionsView() {
|
||||
cancelled: el.getAttribute('data-locale-status-cancelled'),
|
||||
skipped: el.getAttribute('data-locale-status-skipped'),
|
||||
blocked: el.getAttribute('data-locale-status-blocked'),
|
||||
pending: el.getAttribute('data-locale-status-pending'),
|
||||
},
|
||||
logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'),
|
||||
logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
|
||||
|
||||
@@ -19,6 +19,8 @@ export function getActionStatusIcon(status: ActionsStatus, iconVariant: ActionSt
|
||||
case 'cancelled':
|
||||
return {name: 'octicon-stop', colorClass: 'tw-text-text-light'};
|
||||
case 'waiting':
|
||||
return {name: 'octicon-dot-fill', colorClass: 'tw-text-yellow'};
|
||||
case 'pending':
|
||||
return {name: 'octicon-circle', colorClass: 'tw-text-text-light'};
|
||||
case 'blocked':
|
||||
return {name: 'octicon-blocked', colorClass: 'tw-text-yellow'};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
|
||||
export type ActionsStatus = 'unknown' | 'waiting' | 'running' | 'cancelling' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
|
||||
export type ActionsStatus = 'unknown' | 'waiting' | 'running' | 'cancelling' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked' | 'pending';
|
||||
export type ActionsArtifactStatus = 'expired' | 'completed';
|
||||
|
||||
export type ActionsRun = {
|
||||
|
||||
Reference in New Issue
Block a user