fix: improve runner list and details view (#12113)

- shrink runner list width (use icons, move details link to runner name)
- add owner to runner details on admin view
- #11516 removed a lot details which makes it much harder for an admin to find a specific runner

---
### admin list
![image](/attachments/7dd28e5b-6332-48b1-b545-2fc2b83e5368)

### admin org runner details
![image](/attachments/da972377-d401-41fe-8a17-d78824d6d714)

### admin repo runner
![image](/attachments/489e71c2-6087-4441-ad72-695ef0e04161)

### individual list
![image](/attachments/5618b962-0964-415f-a820-e673001f4007)

### individual runner details
![image](/attachments/5799c212-37d5-4047-965f-60952ee7c74c)

### tooltips for edit and delete
![image](/attachments/bcfb9358-0bf3-4d3f-a73c-66fb8e63eb67) ![image](/attachments/63b1c9e5-2fd3-4cc3-9f88-e0a1cf410769)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12113
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-committed-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
Michael Kriese 2026-04-15 20:25:23 +02:00 committed by 0ko
parent 3fe02a2175
commit 1cd81146a9
4 changed files with 85 additions and 66 deletions

View file

@ -516,11 +516,8 @@
"actions.runners.labels": "Labels",
"actions.runners.version": "Version",
"actions.runners.last_online": "Last online time",
"actions.runners.list_runners.details_column": "Details",
"actions.runners.list_runners.edit_column": "Edit",
"actions.runners.list_runners.delete_column": "Delete",
"actions.runners.list_runners.details_button": "Details",
"actions.runners.list_runners.details_button_aria": "Show details of %s",
"actions.runners.list_runners.delete_button": "Delete",
"actions.runners.list_runners.delete_button_aria": "Delete %s",
"actions.runners.list_runners.edit_button": "Edit",

View file

@ -23,6 +23,14 @@
{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}
</dd>
</div>
{{if and $.PageIsAdmin .Runner.BelongsToOwnerName}}
<div class="item">
<dt>{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</dt>
<dd>
{{.Runner.BelongsToOwnerName}}
</dd>
</div>
{{end}}
<div class="item">
<dt>{{ctx.Locale.Tr "actions.runners.labels"}}</dt>
<dd class="tw-flex tw-items-start tw-flex-wrap tw-gap-2">

View file

@ -57,9 +57,6 @@
{{ctx.Locale.Tr "actions.runners.status"}}
{{SortArrow "online" "offline" .SortType false}}
</th>
<th scope="col">
<span class="tw-sr-only">{{ctx.Locale.Tr "actions.runners.list_runners.details_column"}}</span>
</th>
<th scope="col">
<span class="tw-sr-only">{{ctx.Locale.Tr "actions.runners.list_runners.edit_column"}}</span>
</th>
@ -72,7 +69,7 @@
{{range .Runners}}
<tr>
<td>
<div class="tw-font-medium">{{.Name}}</div>
<div class="tw-font-medium"><a href="{{$.Link}}/{{.ID}}" class="runner-action-link" tabindex="0">{{.Name}}</a></div>
<div class="tw-mt-1">{{.UUID}}</div>
</td>
<td class="tw-flex tw-items-start tw-flex-wrap tw-gap-2">
@ -86,6 +83,10 @@
</td>
<td>
{{.BelongsToOwnerType.LocaleString ctx.Locale}}
{{if and $.PageIsAdmin .BelongsToOwnerName}}
<br>
<small>{{.BelongsToOwnerName}}</small>
{{end}}
</td>
<td>
<div class="tw-flex tw-items-center tw-gap-x-2">
@ -107,17 +108,14 @@
</div>
</div>
</td>
<td class="tw-text-right">
<a href="{{$.Link}}/{{.ID}}" class="runner-action-link" tabindex="0" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.details_button_aria" .Name}}">{{ctx.Locale.Tr "actions.runners.list_runners.details_button"}}</a>
</td>
<td class="tw-text-right">
{{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
<a href="{{$.Link}}/{{.ID}}/edit" class="runner-action-link" tabindex="0" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.edit_button_aria" .Name}}">{{ctx.Locale.Tr "actions.runners.list_runners.edit_button"}}</a>
<a href="{{$.Link}}/{{.ID}}/edit" class="runner-action-link" tabindex="0" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.edit_button_aria" .Name}}" data-tooltip-content="{{ctx.Locale.Tr "actions.runners.list_runners.edit_button"}}">{{svg "octicon-pencil"}}</a>
{{end}}
</td>
<td class="tw-text-right">
{{if .Editable $.RunnerOwnerID $.RunnerRepoID}}
<button class="delete-button runner-delete-link" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.delete_button_aria" .Name}}" data-url="{{$.Link}}/{{.ID}}/delete" data-modal-id="runner-delete-modal">{{ctx.Locale.Tr "actions.runners.list_runners.delete_button"}}</button>
<button class="delete-button runner-delete-link" aria-label="{{ctx.Locale.Tr "actions.runners.list_runners.delete_button_aria" .Name}}" data-url="{{$.Link}}/{{.ID}}/delete" data-modal-id="runner-delete-modal" data-tooltip-content="{{ctx.Locale.Tr "actions.runners.list_runners.delete_button"}}">{{svg "octicon-trash"}}</button>
{{end}}
</td>
</tr>

View file

@ -30,39 +30,41 @@ test.describe('Runners of user2', () => {
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Edit Delete');
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17":
- link "runner-2":
- /url: /user/settings/actions/runners/719932
- cell "docker"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-2"
- cell "Edit runner-2"
- cell "Delete runner-2"
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0":
- link "runner-4":
- /url: /user/settings/actions/runners/719934
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell
- cell
`);
await page.getByRole('link', {name: 'Show details of runner-2', exact: true}).click();
await page.getByRole('link', {name: 'runner-2', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-2 .*/);
await page.goto('/user/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
});
test('runner details with tasks of repositories owned by user', async ({page}) => {
await page.goto('/user/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
@ -142,11 +144,12 @@ test.describe('Runners of user2', () => {
await page.getByRole('link', {name: 'List of runners', exact: true}).click();
await expect(page.locator(`tbody tr:has-text("${runnerUUID}")`)).toMatchAriaSnapshot(`
- cell "runner-991301 ${runnerUUID}"
- cell "runner-991301 ${runnerUUID}":
- link "runner-991301":
- /url: /user/settings/actions/runners/\\d+/
- cell ""
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-991301"
- cell "Edit runner-991301"
- cell "Delete runner-991301"
`);
@ -287,67 +290,74 @@ test.describe('Global runners', () => {
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Edit Delete');
await expect(page.locator('tbody tr:has-text("8f940b0b-32a2-479a-9d48-06ab8d8a0b90")')).toMatchAriaSnapshot(`
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90":
- link "runner-1":
- /url: /admin/actions/runners/719931
- cell "debian gpu"
- cell "Organization"
- cell "Organization org3"
- cell "Offline"
- cell "Show details of runner-1"
- cell "Edit runner-1"
- cell "Delete runner-1"
`);
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17":
- link "runner-2":
- /url: /admin/actions/runners/719932
- cell "docker"
- cell "Individual"
- cell "Individual user2"
- cell "Offline"
- cell "Show details of runner-2"
- cell "Edit runner-2"
- cell "Delete runner-2"
`);
await expect(page.locator('tbody tr:has-text("11c9a6da-0a92-46ea-a4f1-b6c98f8c781c")')).toMatchAriaSnapshot(`
- cell "runner-3 11c9a6da-0a92-46ea-a4f1-b6c98f8c781c"
- cell "runner-3 11c9a6da-0a92-46ea-a4f1-b6c98f8c781c":
- link "runner-3":
- /url: /admin/actions/runners/719933
- cell "fedora"
- cell "Organization"
- cell "Organization org17"
- cell "Offline"
- cell "Show details of runner-3"
- cell "Edit runner-3"
- cell "Delete runner-3"
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0":
- link "runner-4":
- /url: /admin/actions/runners/719934
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell "Edit runner-4"
- cell "Delete runner-4"
`);
await expect(page.locator('tbody tr:has-text("69d29449-1de5-4d17-845d-e3ae11a04a1b")')).toMatchAriaSnapshot(`
- cell "runner-5 69d29449-1de5-4d17-845d-e3ae11a04a1b"
- cell "runner-5 69d29449-1de5-4d17-845d-e3ae11a04a1b":
- link "runner-5":
- /url: /admin/actions/runners/719935
- cell "debian"
- cell "Individual"
- cell "Individual user1"
- cell "Offline"
- cell "Show details of runner-5"
- cell "Edit runner-5"
- cell "Delete runner-5"
`);
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toMatchAriaSnapshot(`
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76"
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76":
- link "runner-6":
- /url: /admin/actions/runners/719936
- cell "debian"
- cell "Repository"
- cell "Repository user2/test_workflows"
- cell "Offline"
- cell "Show details of runner-6"
- cell "Edit runner-6"
- cell "Delete runner-6"
`);
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toMatchAriaSnapshot(`
- cell "runner-7 d935307e-1d2d-4b61-8885-bc8a1c52c269"
- cell "runner-7 d935307e-1d2d-4b61-8885-bc8a1c52c269":
- link "runner-7":
- /url: /admin/actions/runners/719937
- cell "alpine"
- cell "Individual"
- cell "Individual user4"
- cell "Offline"
- cell "Show details of runner-7"
- cell "Edit runner-7"
- cell "Delete runner-7"
`);
@ -356,7 +366,7 @@ test.describe('Global runners', () => {
test('runner details with all tasks visible on details page', async ({page}) => {
await page.goto('/admin/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
@ -437,11 +447,12 @@ test.describe('Global runners', () => {
await page.getByRole('link', {name: 'List of runners', exact: true}).click();
await expect(page.locator(`tbody tr:has-text("${runnerUUID}")`)).toMatchAriaSnapshot(`
- cell "runner-473465 ${runnerUUID}"
- cell "runner-473465 ${runnerUUID}":
- link "runner-473465":
- /url: /admin/actions/runners/\\d+/
- cell ""
- cell "Global"
- cell "Offline"
- cell "Show details of runner-473465"
- cell "Edit runner-473465"
- cell "Delete runner-473465"
`);
@ -546,22 +557,24 @@ test.describe('Organization runners', () => {
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Edit Delete');
await expect(page.locator('tbody tr:has-text("8f940b0b-32a2-479a-9d48-06ab8d8a0b90")')).toMatchAriaSnapshot(`
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90"
- cell "runner-1 8f940b0b-32a2-479a-9d48-06ab8d8a0b90":
- link "runner-1":
- /url: /org/org3/settings/actions/runners/719931
- cell "debian gpu"
- cell "Organization"
- cell "Offline"
- cell "Show details of runner-1"
- cell "Edit runner-1"
- cell "Delete runner-1"
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0":
- link "runner-4":
- /url: /org/org3/settings/actions/runners/719934
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell
- cell
`);
@ -572,19 +585,19 @@ test.describe('Organization runners', () => {
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toBeHidden();
// Verify that details of usable runners are accessible.
await page.getByRole('link', {name: 'Show details of runner-1', exact: true}).click();
await page.getByRole('link', {name: 'runner-1', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-1 .*/);
await page.goto('/org/org3/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
});
test('runner details with tasks of repositories owned by organization', async ({page}) => {
await page.goto('/org/org3/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();
@ -621,7 +634,7 @@ test.describe('Organization runners', () => {
test('runner details with multiple pages of tasks', async ({page}) => {
await page.goto('/org/org3/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-1', exact: true}).click();
await page.getByRole('link', {name: 'runner-1', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-1 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-1'})).toBeVisible();
@ -664,31 +677,34 @@ test.describe('Repository runners', () => {
// We cannot assert the length of the table because it's influenced by global fixtures. It also changes depending on
// the ordering of tests.
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Details Edit Delete');
await expect(rows.nth(0)).toHaveAccessibleName('Name Labels Type Status Edit Delete');
await expect(page.locator('tbody tr:has-text("3a20ad8d-d5d6-4b7b-ba55-841ac8264c17")')).toMatchAriaSnapshot(`
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17"
- cell "runner-2 3a20ad8d-d5d6-4b7b-ba55-841ac8264c17":
- link "runner-2":
- /url: /user2/test_workflows/settings/actions/runners/719932
- cell "docker"
- cell "Individual"
- cell "Offline"
- cell "Show details of runner-2"
- cell
- cell
`);
await expect(page.locator('tbody tr:has-text("1ef59b64-93b7-4ad4-ade4-21ca13db49c0")')).toMatchAriaSnapshot(`
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0"
- cell "runner-4 1ef59b64-93b7-4ad4-ade4-21ca13db49c0":
- link "runner-4":
- /url: /user2/test_workflows/settings/actions/runners/719934
- cell "docker"
- cell "Global"
- cell "Offline"
- cell "Show details of runner-4"
- cell
- cell
`);
await expect(page.locator('tbody tr:has-text("9da25fbb-89a5-4520-a35a-d55fc94e4b76")')).toMatchAriaSnapshot(`
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76"
- cell "runner-6 9da25fbb-89a5-4520-a35a-d55fc94e4b76":
- link "runner-6":
- /url: /user2/test_workflows/settings/actions/runners/719936
- cell "debian"
- cell "Repository"
- cell "Offline"
- cell "Show details of runner-6"
- cell "Edit runner-6"
- cell "Delete runner-6"
`);
@ -697,24 +713,24 @@ test.describe('Repository runners', () => {
await expect(page.locator('tbody tr:has-text("d935307e-1d2d-4b61-8885-bc8a1c52c269")')).toBeHidden();
// Verify that details of usable runners are accessible.
await page.getByRole('link', {name: 'Show details of runner-2', exact: true}).click();
await page.getByRole('link', {name: 'runner-2', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-2 .*/);
await page.goto('/user2/test_workflows/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await page.goto('/user2/test_workflows/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-6', exact: true}).click();
await page.getByRole('link', {name: 'runner-6', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-6 .*/);
});
test('runner details with tasks of repository only', async ({page}) => {
await page.goto('/user2/test_workflows/settings/actions/runners');
await page.getByRole('link', {name: 'Show details of runner-4', exact: true}).click();
await page.getByRole('link', {name: 'runner-4', exact: true}).click();
await expect(page).toHaveTitle(/^Runner runner-4 .*/);
await expect(page.getByRole('heading', {name: 'Runner runner-4'})).toBeVisible();