Workflow Artifact Info Hover (#37100)

Add expiry metadata to action artifacts in the run view and show it on hover.

---------

Signed-off-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Nicolas
2026-04-19 09:37:50 +02:00
committed by GitHub
parent 0bc2a2836f
commit 16bdae53c8
16 changed files with 157 additions and 30 deletions
@@ -0,0 +1,34 @@
import {buildArtifactTooltipHtml} from './ActionRunArtifacts.ts';
import {normalizeTestHtml} from '../utils/testhelper.ts';
describe('buildArtifactTooltipHtml', () => {
test('active artifact', () => {
const result = buildArtifactTooltipHtml({
name: 'artifact.zip',
size: 1024 * 1024,
status: 'completed',
expiresUnix: Date.UTC(2026, 2, 20, 12, 0, 0) / 1000,
}, 'Expires at %s (extra)');
expect(normalizeTestHtml(result)).toBe(normalizeTestHtml(`<span class="flex-text-inline">
<span>Expires at </span>
<relative-time datetime="2026-03-20T12:00:00.000Z" threshold="P0Y" prefix="" weekday="" year="numeric" month="short" hour="numeric" minute="2-digit">
2026-03-20T12:00:00.000Z
</relative-time>
<span> (extra)</span>
<span class="inline-divider">,</span>
<span>1.0 MiB</span>
</span>
`));
});
test('no expiry', () => {
const result = buildArtifactTooltipHtml({
name: 'artifact.zip',
size: 512,
status: 'completed',
expiresUnix: 0,
}, 'Expires at %s');
expect(normalizeTestHtml(result)).toBe(`<span class="flex-text-inline">512 B</span>`);
});
});
@@ -0,0 +1,25 @@
import {html} from '../utils/html.ts';
import {formatBytes} from '../utils.ts';
import type {ActionsArtifact} from '../modules/gitea-actions.ts';
export function buildArtifactTooltipHtml(artifact: ActionsArtifact, expiresAtLocale: string): string {
const sizeText = formatBytes(artifact.size);
if (artifact.expiresUnix <= 0) {
return html`<span class="flex-text-inline">${sizeText}</span>`; // use the same layout as below
}
// split so the <relative-time> element can be interleaved, e.g. "Expires at %s" -> ["Expires at ", ""]
const [prefix, suffix = ''] = expiresAtLocale.split('%s');
const datetime = new Date(artifact.expiresUnix * 1000).toISOString();
return html`
<span class="flex-text-inline">
<span>${prefix}</span>
<relative-time datetime="${datetime}" threshold="P0Y" prefix="" weekday="" year="numeric" month="short" hour="numeric" minute="2-digit">
${datetime}
</relative-time>
<span>${suffix}</span>
<span class="inline-divider">,</span>
<span>${sizeText}</span>
</span>
`;
}
+16 -8
View File
@@ -5,7 +5,8 @@ import {toRefs} from 'vue';
import {POST, DELETE} from '../modules/fetch.ts';
import ActionRunSummaryView from './ActionRunSummaryView.vue';
import ActionRunJobView from './ActionRunJobView.vue';
import {createActionRunViewStore} from "./ActionRunView.ts";
import {createActionRunViewStore} from './ActionRunView.ts';
import {buildArtifactTooltipHtml} from './ActionRunArtifacts.ts';
defineOptions({
name: 'RepoActionView',
@@ -20,7 +21,7 @@ const props = defineProps<{
const locale = props.locale;
const store = createActionRunViewStore(props.actionsUrl, props.runId);
const {currentRun: run , runArtifacts: artifacts} = toRefs(store.viewData);
const {currentRun: run, runArtifacts: artifacts} = toRefs(store.viewData);
function cancelRun() {
POST(`${run.value.link}/cancel`);
@@ -120,18 +121,24 @@ async function deleteArtifact(name: string) {
<ul class="ui relaxed list flex-items-block">
<li class="item" v-for="artifact in artifacts" :key="artifact.name">
<template v-if="artifact.status !== 'expired'">
<a class="tw-flex-1 flex-text-block" target="_blank" :href="run.link+'/artifacts/'+artifact.name">
<SvgIcon name="octicon-file" class="tw-text-text"/>
<a
class="tw-flex-1 flex-text-block muted" target="_blank"
:href="run.link+'/artifacts/'+encodeURIComponent(artifact.name)"
:data-tooltip-content="buildArtifactTooltipHtml(artifact, locale.artifactExpiresAt)"
data-tooltip-render="html"
data-tooltip-placement="top-end"
>
<SvgIcon name="octicon-file" class="tw-text-text-light"/>
<span class="tw-flex-1 gt-ellipsis">{{ artifact.name }}</span>
</a>
<a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)">
<SvgIcon name="octicon-trash" class="tw-text-text"/>
<a v-if="run.canDeleteArtifact" class="muted" @click="deleteArtifact(artifact.name)">
<SvgIcon name="octicon-trash"/>
</a>
</template>
<span v-else class="flex-text-block tw-flex-1 tw-text-grey-light">
<span v-else class="flex-text-block tw-flex-1 tw-text-text-light-2">
<SvgIcon name="octicon-file"/>
<span class="tw-flex-1 gt-ellipsis">{{ artifact.name }}</span>
<span class="ui label tw-text-grey-light tw-flex-shrink-0">{{ locale.artifactExpired }}</span>
<span class="ui label tw-flex-shrink-0">{{ locale.artifactExpired }}</span>
</span>
</li>
</ul>
@@ -251,6 +258,7 @@ async function deleteArtifact(name: string) {
.left-list-header {
font-size: 13px;
font-weight: var(--font-weight-semibold);
color: var(--color-text-light-2);
}