mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-02 15:10:43 +02:00
feat(actions): add build queue view (#38585)
Adds a read-only Actions job queue: running jobs first, then waiting jobs in the order a runner picks them up. It is shown instance-wide in the admin Actions section with owner, repository and status filters, and per repository in the Actions tab. Both lists refresh in place. Pending work is currently only visible per repository and newest-first, so nothing shows what is queued, in which order, or what occupies the runners. Reordering the queue will be proposed separately. A migration adds indexes for the runner pickup query and repository-scoped status lookups. * Fix #34198 <img width="1345" height="451" alt="image" src="https://github.com/user-attachments/assets/7d52ff76-81b4-44e8-b583-d7d89c9dffcd" /> <img width="1809" height="1134" alt="image" src="https://github.com/user-attachments/assets/4d56c0cb-bae7-4ce2-8f3c-75163b2bc7f4" /> --------- Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
30 files changed
+928
-95
No files matched your search
@@ -844,8 +844,8 @@ table th[data-sortt-desc] .svg {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui.list.flex-items-block > .item,
|
||||
.ui.vertical.menu .item.flex-text-block,
|
||||
.ui.ui.ui.ui .item.flex-text-block, /* override .ui.list and .ui.vertical.menu */
|
||||
.ui.ui.ui.ui.flex-items-block > .item,
|
||||
.ui.form .field > label.flex-text-block, /* override fomantic "block" style */
|
||||
.flex-items-block > .item,
|
||||
.flex-text-block {
|
||||
|
||||
@@ -817,10 +817,6 @@ select.ui.dropdown {
|
||||
max-height: 2em;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu > .item > svg {
|
||||
margin-right: 0.78rem;
|
||||
}
|
||||
|
||||
/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
|
||||
.ui.dropdown > .text > .img {
|
||||
margin-left: 0;
|
||||
@@ -893,7 +889,8 @@ select.ui.dropdown {
|
||||
}
|
||||
|
||||
/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content
|
||||
the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */
|
||||
the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden
|
||||
TODO: rename it to "flex-items-dropdown" in the future since it is only used for dropdown menu items, not for other menu items */
|
||||
.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
@@ -901,6 +898,11 @@ the "!important" is necessary to override Fomantic UI menu item styles, meanwhil
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* TODO: MENU-ITEM-FLEX-MARGIN: need to refactor: flex menu items don't need svg margin, too many patches (see below ...) */
|
||||
.ui.dropdown .menu:not(.flex-items-menu) > .item:not(.flex-text-block, .flex-text-inline) > .svg {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ui.dropdown .menu.flex-items-menu > .item img,
|
||||
.ui.dropdown .menu.flex-items-menu > .item svg {
|
||||
margin: 0;
|
||||
|
||||
@@ -55,8 +55,9 @@
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.ui.menu .item > .svg {
|
||||
margin-right: 0.35em;
|
||||
/* TODO: MENU-ITEM-FLEX-MARGIN: need to refactor: flex menu items don't need svg margin */
|
||||
.ui.menu:not(.flex-items-block) .item:not(.flex-text-block, .flex-text-inline) > .svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ui.menu .item > a:not(.ui) {
|
||||
|
||||
@@ -3,8 +3,7 @@ import RepoActionView from '../components/RepoActionView.vue';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {html} from '../utils/html.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {activePageTimerRefresh, createElementFromHTML, protectMorphElements, recoverMorphElements} from '../utils/dom.ts';
|
||||
import {Idiomorph} from 'idiomorph';
|
||||
import {activePageTimerRefresh, createElementFromHTML, morphElementWithProtection} from '../utils/dom.ts';
|
||||
|
||||
export function updateWorkflowBadgeFields(form: HTMLElement, branch: string): void {
|
||||
const badgeURLParsed = new URL(form.getAttribute('data-badge-url')!);
|
||||
@@ -31,6 +30,7 @@ export function initRepositoryActions() {
|
||||
registerGlobalInitFunc('initWorkflowBadgeForm', initWorkflowBadgeForm);
|
||||
initRepositoryActionsView();
|
||||
registerGlobalInitFunc('initActionRunsList', initActionRunsList);
|
||||
registerGlobalInitFunc('initActionJobQueueList', initActionJobQueueList);
|
||||
}
|
||||
|
||||
function initRepositoryActionsView() {
|
||||
@@ -115,22 +115,19 @@ function initActionRunsList(el: HTMLElement) {
|
||||
interval: () => Number(el.getAttribute('data-action-runs-refresh-interval')),
|
||||
async callback() {
|
||||
const resp = await GET(el.getAttribute('data-action-runs-refresh-link')!);
|
||||
if (!resp.ok || resp.status !== 200) return;
|
||||
|
||||
const newEl = createElementFromHTML(await resp.text());
|
||||
for (const attr of newEl.attributes) el.setAttribute(attr.name, attr.value);
|
||||
for (const newItem of newEl.querySelectorAll(':scope > .item')) {
|
||||
const oldItem = el.querySelector(`#${newItem.id}`);
|
||||
if (!oldItem) continue;
|
||||
|
||||
// If the end user is operating the row, then don't refresh its content.
|
||||
// Otherwise, there will be more edge cases and inconsistencies, e.g.: dropdown still shows old items but the icon has changed.
|
||||
if (oldItem.querySelector('.ui.dropdown.active')) continue;
|
||||
|
||||
const protectedElems = protectMorphElements(newItem);
|
||||
Idiomorph.morph(oldItem, newItem, {morphStyle: 'outerHTML'});
|
||||
recoverMorphElements(el.querySelector(`#${newItem.id}`)!, protectedElems);
|
||||
}
|
||||
if (!resp.ok) return;
|
||||
morphElementWithProtection(el, createElementFromHTML(await resp.text()), {morphStyle: 'outerHTML'});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initActionJobQueueList(el: HTMLElement) {
|
||||
activePageTimerRefresh({
|
||||
interval: () => Number(el.getAttribute('data-job-queue-refresh-interval')),
|
||||
async callback() {
|
||||
const resp = await GET(el.getAttribute('data-job-queue-refresh-link')!);
|
||||
if (!resp.ok) return;
|
||||
morphElementWithProtection(el, createElementFromHTML(await resp.text()), {morphStyle: 'outerHTML'});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
createElementFromAttrs, createElementFromHTML,
|
||||
queryElemChildren, querySingleVisibleElem,
|
||||
protectMorphElements, recoverMorphElements,
|
||||
toggleElem,
|
||||
toggleElem, morphElementWithProtection,
|
||||
} from './dom.ts';
|
||||
|
||||
test('createElementFromHTML', () => {
|
||||
@@ -70,3 +70,33 @@ test('protectMorphElements', () => {
|
||||
recoverMorphElements(el, protectedElems);
|
||||
expect(el.outerHTML).toEqual('<div><span data-morph-protect="">foo</span></div>');
|
||||
});
|
||||
|
||||
describe('morphElementWithProtection', () => {
|
||||
const newElHtml = '<div><div><span>new</span><div class="ui dropdown">changed</div></div></div>';
|
||||
it('morph normal', () => {
|
||||
const el = createElementFromHTML('<div><div><span>old</span><div class="ui dropdown active"></div></div></div>');
|
||||
const newEl = createElementFromHTML(newElHtml);
|
||||
morphElementWithProtection(el, newEl, {morphStyle: 'outerHTML'});
|
||||
// span is changed, but dropdown is skipped because it is active
|
||||
expect(el.outerHTML).toEqual('<div><div><span>new</span><div class="ui dropdown active"></div></div></div>');
|
||||
});
|
||||
it('morph whole', () => {
|
||||
const el = createElementFromHTML('<div><div data-morph-whole><span>old</span><div class="ui dropdown active"></div></div></div>');
|
||||
const newEl = createElementFromHTML(newElHtml);
|
||||
morphElementWithProtection(el, newEl, {morphStyle: 'outerHTML'});
|
||||
// span is not changed, because the parent div is marked as data-morph-whole and there is an active dropdown, so it is skipped
|
||||
expect(el.outerHTML).toEqual('<div><div data-morph-whole=""><span>old</span><div class="ui dropdown active"></div></div></div>');
|
||||
});
|
||||
it('morph protection', () => {
|
||||
const el = createElementFromHTML('<div><span></span><div class="ui dropdown"></div></div>');
|
||||
const newEl = createElementFromHTML('<div><span>new</span><div class="ui dropdown">changed</div></div>');
|
||||
const elSpanOld = el.querySelector('span');
|
||||
const elDropdownOld = el.querySelector('.ui.dropdown');
|
||||
const morphedEl = morphElementWithProtection(el, newEl, {morphStyle: 'outerHTML'});
|
||||
expect(el.outerHTML).toEqual('<div><span>new</span><div class="ui dropdown">changed</div></div>');
|
||||
const elSpanNew = morphedEl.querySelector('span');
|
||||
const elDropdownNew = morphedEl.querySelector('.ui.dropdown');
|
||||
expect(elSpanNew).toBe(elSpanOld); // span is morphed in place, so it is the same element
|
||||
expect(elDropdownNew).not.toBe(elDropdownOld); // dropdown is protected and fully replaced, so it is a new element
|
||||
});
|
||||
});
|
||||
+32
-1
@@ -1,6 +1,7 @@
|
||||
import {debounce} from './func.ts';
|
||||
import type {Promisable} from '../types.ts';
|
||||
import type $ from 'jquery';
|
||||
import {Idiomorph} from 'idiomorph';
|
||||
|
||||
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
||||
type ElementArg = Element | string | ArrayLikeIterable<Element> | ReturnType<typeof $>;
|
||||
@@ -424,6 +425,36 @@ export function recoverMorphElements(el: Element, protectedElems: ProtectedMorph
|
||||
for (const [id, html] of Object.entries(protectedElems)) {
|
||||
const it = el.querySelector(`[data-morph-protect="${CSS.escape(id)}"]`);
|
||||
if (!it) continue;
|
||||
it.outerHTML = html;
|
||||
it.replaceWith(createElementFromHTML(html));
|
||||
}
|
||||
}
|
||||
|
||||
export type MorphElementOptions = {
|
||||
morphStyle: 'innerHTML' | 'outerHTML';
|
||||
};
|
||||
|
||||
export function morphElementWithProtection(el: Element, newEl: Element, opts: MorphElementOptions): Element {
|
||||
const protectedElems = protectMorphElements(newEl);
|
||||
const selectorSkipElems = '.ui.dropdown.active';
|
||||
const nodes = Idiomorph.morph(el, newEl, {
|
||||
morphStyle: opts.morphStyle,
|
||||
callbacks: {
|
||||
beforeNodeMorphed: (oldNode /* , newNode */) => {
|
||||
if (!(oldNode instanceof Element)) return true;
|
||||
|
||||
// If the end user is operating a row, then don't refresh its content.
|
||||
// Otherwise, there will be more edge cases and inconsistencies, e.g.: dropdown still shows old items but the icon has changed.
|
||||
const oldNodeMorphWholeAndSkipChild = oldNode.matches('[data-morph-whole]') && oldNode.querySelector(selectorSkipElems);
|
||||
|
||||
// If the element should be skipped, don't morph it
|
||||
const oldNodeShouldSkip = oldNode.matches(selectorSkipElems);
|
||||
|
||||
const shouldSkip = oldNodeMorphWholeAndSkipChild || oldNodeShouldSkip;
|
||||
return !shouldSkip;
|
||||
},
|
||||
},
|
||||
});
|
||||
const morphedElem = nodes[0] as Element;
|
||||
recoverMorphElements(morphedElem, protectedElems);
|
||||
return morphedElem;
|
||||
}
|
||||
Reference in new issue
Block a user