Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion models/actions/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ type ActionArtifactMeta struct {
ArtifactName string
FileSize int64
Status ArtifactStatus
ExpiredUnix timeutil.TimeStamp
}

// ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run
Expand All @@ -191,7 +192,7 @@ func ListUploadedArtifactsMeta(ctx context.Context, repoID, runID int64) ([]*Act
return arts, db.GetEngine(ctx).Table("action_artifact").
Where("repo_id=? AND run_id=? AND (status=? OR status=?)", repoID, runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired).
GroupBy("artifact_name").
Select("artifact_name, sum(file_size) as file_size, max(status) as status").
Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix").
Find(&arts)
}

Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"unpin": "Unpin",
"artifacts": "Artifacts",
"expired": "Expired",
"artifact_expires_at": "Expires at %s",
"confirm_delete_artifact": "Are you sure you want to delete the artifact '%s'?",
"archived": "Archived",
"concept_system_global": "Global",
Expand Down
28 changes: 16 additions & 12 deletions routers/web/devtest/mock_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,24 +96,28 @@ func MockActionsRunsJobs(ctx *context.Context) {
},
}
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-a",
Size: 100 * 1024,
Status: "expired",
Name: "artifact-a",
Size: 100 * 1024,
Status: "expired",
ExpiresUnix: time.Now().Add(-24 * time.Hour).Unix(),
})
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-b",
Size: 1024 * 1024,
Status: "completed",
Name: "artifact-b",
Size: 1024 * 1024,
Status: "completed",
ExpiresUnix: time.Now().Add(24 * time.Hour).Unix(),
})
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
Size: 100 * 1024,
Status: "expired",
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
Size: 100 * 1024,
Status: "expired",
ExpiresUnix: time.Now().Add(-24 * time.Hour).Unix(),
})
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
Size: 1024 * 1024,
Status: "completed",
Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
Size: 1024 * 1024,
Status: "completed",
ExpiresUnix: time.Now().Add(24 * time.Hour).Unix(),
})

resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
Expand Down
14 changes: 8 additions & 6 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,10 @@ type ViewRequest struct {
}

type ArtifactsViewItem struct {
Name string `json:"name"`
Size int64 `json:"size"`
Status string `json:"status"`
Name string `json:"name"`
Size int64 `json:"size"`
Status string `json:"status"`
ExpiresUnix int64 `json:"expiresUnix"`
}

type ViewResponse struct {
Expand Down Expand Up @@ -344,9 +345,10 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifact
}
for _, art := range artifacts {
artifactsViewItems = append(artifactsViewItems, &ArtifactsViewItem{
Name: art.ArtifactName,
Size: art.FileSize,
Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"),
Name: art.ArtifactName,
Size: art.FileSize,
Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"),
ExpiresUnix: int64(art.ExpiredUnix),
})
}
return artifactsViewItems, nil
Expand Down
1 change: 1 addition & 0 deletions templates/devtest/relative-time.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<div>numeric: <relative-time datetime="2024-03-11" threshold="P0Y" prefix="" weekday="" year="" day="numeric" month="numeric"></relative-time></div>
<div>weekday: <relative-time datetime="2024-03-11" threshold="P0Y" prefix="" weekday="long" year="" month="numeric"></relative-time></div>
<div>with time: <relative-time datetime="2024-03-11T19:00:00-05:00" threshold="P0Y" prefix="" weekday="long" year="" month="numeric"></relative-time></div>
<div>minutes: <relative-time datetime="2024-03-11T19:30:45-05:00" threshold="P0Y" prefix="" weekday="" year="numeric" month="short" hour="numeric" minute="2-digit"></relative-time></div>
</div>
<div>
<h2>Threshold</h2>
Expand Down
1 change: 1 addition & 0 deletions templates/repo/actions/view_component.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}"
data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}"
data-locale-artifact-expired="{{ctx.Locale.Tr "expired"}}"
data-locale-artifact-expires-at="{{ctx.Locale.Tr "artifact_expires_at"}}"
data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}"
data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}"
data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}"
Expand Down
33 changes: 33 additions & 0 deletions web_src/js/components/ActionRunArtifacts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {createArtifactTooltipElement} from './ActionRunArtifacts.ts';

test('createArtifactTooltipElement for active artifact', () => {
const el = createArtifactTooltipElement({
name: 'artifact.zip',
size: 1024 * 1024,
status: 'completed',
expiresUnix: Date.UTC(2026, 2, 20, 12, 0, 0) / 1000,
}, 'Expires at %s');

const rt = el.querySelector('relative-time')!;
expect(rt).not.toBeNull();
expect(rt.getAttribute('datetime')).toBe('2026-03-20T12:00:00.000Z');
expect(rt.getAttribute('threshold')).toBe('P0Y');
expect(rt.getAttribute('month')).toBe('short');
expect(rt.getAttribute('hour')).toBe('numeric');
expect(rt.getAttribute('minute')).toBe('2-digit');
expect(el.textContent).toContain('Expires at');
expect(el.textContent).toContain('1.0 MiB');
expect(el.querySelector('.artifact-size')).not.toBeNull();
});

test('createArtifactTooltipElement with no expiry', () => {
const el = createArtifactTooltipElement({
name: 'artifact.zip',
size: 512,
status: 'completed',
expiresUnix: 0,
}, 'Expires at %s');

expect(el.querySelector('relative-time')).toBeNull();
expect(el.textContent).toBe('512 B');
});
20 changes: 20 additions & 0 deletions web_src/js/components/ActionRunArtifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {createElementFromAttrs} from '../utils/dom.ts';
import {formatBytes} from '../utils.ts';
import type {ActionsArtifact} from '../modules/gitea-actions.ts';

export function createArtifactTooltipElement(artifact: ActionsArtifact, expiresAtLocale: string): HTMLElement {
const sizeText = formatBytes(artifact.size);

if (artifact.expiresUnix <= 0) {
return createElementFromAttrs('span', null, sizeText);
}

const datetime = new Date(artifact.expiresUnix * 1000).toISOString();
const parts = expiresAtLocale.split('%s');
const relativeTime = createElementFromAttrs('relative-time', {
datetime, threshold: 'P0Y', prefix: '', weekday: '',
year: 'numeric', month: 'short', hour: 'numeric', minute: '2-digit',
});
const sizeSpan = createElementFromAttrs('span', {class: 'artifact-size tw-border-l tw-border-current tw-ml-2 tw-pl-2'}, sizeText);
return createElementFromAttrs('span', null, parts[0] ?? '', relativeTime, parts[1] ?? '', sizeSpan);
}
40 changes: 31 additions & 9 deletions web_src/js/components/RepoActionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ 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 {createArtifactTooltipElement} from './ActionRunArtifacts.ts';
import {createTippy} from '../modules/tippy.ts';

defineOptions({
name: 'RepoActionView',
Expand All @@ -20,7 +22,16 @@ 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 initSidebarTooltip(el: HTMLElement | null, content: string | HTMLElement) {
if (!el) return;
if (el._tippy) {
el._tippy.setContent(content);
} else {
createTippy(el, {content, role: 'tooltip', theme: 'tooltip', placement: 'top-end', arrow: false, offset: [0, 2]});
}
}
Comment on lines +27 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to improve the framework, instead of making future developers copy&paste such code.

For example: data-tooltip-render-html="true"

Copy link
Copy Markdown
Member

@silverwind silverwind Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess some generic way to specify all tippy props in HTML is needed. Should be done with a future migration to https://github.com/floating-ui/floating-ui in mind.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess some generic way to specify all tippy props in HTML is needed.

No, don't do more than it needs.

"tooltip" is "tooltip". Other tippy popups don't use "tooltip-content" for content.


function cancelRun() {
POST(`${run.value.link}/cancel`);
Expand Down Expand Up @@ -118,20 +129,20 @@ async function deleteArtifact(name: string) {
<div class="ui divider"/>
<div class="left-list-header">{{ locale.artifactsTitle }} ({{ artifacts.length }})</div>
<ul class="ui relaxed list flex-items-block">
<li class="item" v-for="artifact in artifacts" :key="artifact.name">
<li :ref="artifact.status !== 'expired' ? (el) => initSidebarTooltip(el as HTMLElement, createArtifactTooltipElement(artifact, locale.artifactExpiresAt)) : undefined" 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)">
<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>
Expand Down Expand Up @@ -251,6 +262,7 @@ async function deleteArtifact(name: string) {

.left-list-header {
font-size: 13px;
font-weight: var(--font-weight-semibold);
color: var(--color-text-light-2);
}

Expand All @@ -259,6 +271,16 @@ async function deleteArtifact(name: string) {
padding-left: 10px;
}

.action-view-left .ui.relaxed.list > .item {
padding-top: 0;
padding-bottom: 0;
}

.action-view-left .ui.relaxed.list > .item > :first-child {
padding-top: 0.42857143em;
padding-bottom: 0.42857143em;
}

Comment on lines +274 to +283
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, don't use such trick. You can see it breaks the UI , makes the "job list" very narrow.

Copy link
Copy Markdown
Member

@silverwind silverwind Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor fix to have the full vertical area hoverable (e.g. link turns blue on .item mouseenter), but I think it needs a cleaner solution. In my testing, visually there was no difference, padding just moved from .item to .item > a.

Will check. Likely refactor to a.item.

.job-brief-item {
padding: 6px 10px;
border-radius: var(--border-radius);
Expand Down
1 change: 1 addition & 0 deletions web_src/js/features/repo-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function initRepositoryActionView() {
artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
areYouSure: el.getAttribute('data-locale-are-you-sure'),
artifactExpired: el.getAttribute('data-locale-artifact-expired'),
artifactExpiresAt: el.getAttribute('data-locale-artifact-expires-at'),
confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
Expand Down
2 changes: 2 additions & 0 deletions web_src/js/modules/gitea-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ export type ActionsJob = {

export type ActionsArtifact = {
name: string;
size: number;
status: string;
expiresUnix: number;
};
13 changes: 12 additions & 1 deletion web_src/js/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
dirname, basename, extname, isObject, stripTags, parseIssueHref,
dirname, basename, extname, formatBytes, isObject, stripTags, parseIssueHref,
parseUrl, translateMonth, translateDay, blobToDataURI,
toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseRepoOwnerPathInfo,
urlQueryEscape,
Expand Down Expand Up @@ -134,6 +134,17 @@ test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a'));
});

test('formatBytes', () => {
expect(formatBytes(-1)).toBe('0 B');
expect(formatBytes(0)).toBe('0 B');
expect(formatBytes(512)).toBe('512 B');
expect(formatBytes(1024)).toBe('1.0 KiB');
expect(formatBytes(1536)).toBe('1.5 KiB');
expect(formatBytes(10 * 1024)).toBe('10 KiB');
expect(formatBytes(1024 * 1024)).toBe('1.0 MiB');
expect(formatBytes(1024 * 1024 * 1024)).toBe('1.0 GiB');
});

test('file detection', () => {
for (const name of ['a.avif', 'a.jpg', '/a.jpeg', '.file.png', '.webp', 'file.svg']) {
expect(isImageFile({name})).toBeTruthy();
Expand Down
11 changes: 11 additions & 0 deletions web_src/js/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ export function isVideoFile({name, type}: {name?: string, type?: string}): boole
return Boolean(/\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'));
}

const byteUnits = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];

export function formatBytes(num: number, precision = 2): string {
if (!Number.isFinite(num) || num < 0) return `0 ${byteUnits[0]}`;
if (num < 1024) return `${num} ${byteUnits[0]}`;
const exp = Math.min(Math.floor(Math.log2(num) / 10), byteUnits.length - 1);
const value = num / (1024 ** exp);
const digits = Math.max(0, precision - 1 - Math.floor(Math.log10(value)));
return `${value.toFixed(digits)} ${byteUnits[exp]}`;
}

export function toggleFullScreen(fullScreenEl: HTMLElement, isFullScreen: boolean, sourceParentSelector?: string): void {
// hide other elements
const headerEl = document.querySelector('#navbar')!;
Expand Down