0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-05 07:16:25 +02:00
Nicolas dd59c68486
feat(actions): bulk delete, disable and enable runners in admin UI (#37869)
Adds bulk actions on the site-admin runner list
(`/-/admin/actions/runners`). Site admins can now select multiple
runners and **Delete**, **Disable**, or **Enable** them in one go
instead of clicking through each runner's edit page.

Scope is intentionally limited to the admin page. The user, org, and
repo runner pages keep their existing per-row UX — the shared list
template gates the bulk UI behind an `AllowBulkActions` flag set only by
the admin handler.

## Screenshots

<img width="1582" height="353"
src="https://github.com/user-attachments/assets/2125661f-aac0-4168-990a-97995a26abd2"
/>

---------

Signed-off-by: Nicolas <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-05-29 22:16:47 +02:00

329 lines
14 KiB
TypeScript

import {checkAppUrl} from '../common-page.ts';
import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts';
import {POST} from '../../modules/fetch.ts';
import {showFomanticModal} from '../../modules/fomantic/modal.ts';
import {pathEscape} from '../../utils/url.ts';
import {registerGlobalInitFunc} from '../../modules/observer.ts';
const {appSubUrl} = window.config;
function onSecurityProtocolChange(): void {
if (Number(document.querySelector<HTMLInputElement>('#security_protocol')?.value) > 0) {
showElem('.has-tls');
} else {
hideElem('.has-tls');
}
}
export function initAdminCommon(): void {
if (!document.querySelector('.page-content.admin')) return;
// check whether appUrl(ROOT_URL) is correct, if not, show an error message
checkAppUrl();
initAdminUser();
initAdminAuthentication();
initAdminNotice();
registerGlobalInitFunc('initRunnerBulkToolbar', initAdminRunnerBulk);
}
function initAdminRunnerBulk(toolbar: HTMLElement) {
const actionButtons = toolbar.querySelectorAll<HTMLButtonElement>('.runner-bulk-action');
const formRunnerIds = toolbar.querySelector<HTMLInputElement>('form input[name="ids"]')!;
const rowCheckboxes = document.querySelectorAll<HTMLInputElement>('.runner-bulk-select');
const selectAll = document.querySelector<HTMLInputElement>('.runner-bulk-select-all');
if (!selectAll) return;
const refresh = () => {
const checked = Array.from(rowCheckboxes).filter((c) => c.checked);
toggleElem(toolbar, checked.length > 0);
for (const btn of actionButtons) {
btn.querySelector<HTMLElement>('.runner-bulk-count')!.textContent = `(${checked.length})`;
}
selectAll.checked = checked.length > 0 && checked.length === rowCheckboxes.length;
selectAll.indeterminate = checked.length > 0 && checked.length < rowCheckboxes.length;
};
selectAll.addEventListener('change', () => {
for (const cb of rowCheckboxes) cb.checked = selectAll.checked;
refresh();
});
for (const cb of rowCheckboxes) cb.addEventListener('change', refresh);
refresh();
const collectSelectedIds = () => {
const ids = [];
for (const cb of rowCheckboxes) {
if (cb.checked) ids.push(cb.getAttribute('data-runner-id')!);
}
return ids.join(',');
};
formRunnerIds.value = collectSelectedIds();
}
function initAdminUser() {
const pageContent = document.querySelector('.page-content.admin.edit.user, .page-content.admin.new.user');
if (!pageContent) return;
document.querySelector<HTMLInputElement>('#login_type')?.addEventListener('change', function () {
if (this.value?.startsWith('0')) {
document.querySelector<HTMLInputElement>('#user_name')?.removeAttribute('disabled');
document.querySelector<HTMLInputElement>('#login_name')?.removeAttribute('required');
hideElem('.non-local');
showElem('.local');
document.querySelector<HTMLInputElement>('#user_name')?.focus();
if (this.getAttribute('data-password') === 'required') {
document.querySelector('#password')?.setAttribute('required', 'required');
}
} else {
if (document.querySelector<HTMLDivElement>('.admin.edit.user')) {
document.querySelector<HTMLInputElement>('#user_name')?.setAttribute('disabled', 'disabled');
}
document.querySelector<HTMLInputElement>('#login_name')?.setAttribute('required', 'required');
showElem('.non-local');
hideElem('.local');
document.querySelector<HTMLInputElement>('#login_name')?.focus();
document.querySelector<HTMLInputElement>('#password')?.removeAttribute('required');
}
});
}
function initAdminAuthentication() {
const pageContent = document.querySelector('.page-content.admin.authentication');
if (!pageContent) return;
const isNewPage = pageContent.classList.contains('new');
const isEditPage = pageContent.classList.contains('edit');
if (!isNewPage && !isEditPage) return;
function onUsePagedSearchChange() {
const searchPageSizeElements = document.querySelectorAll<HTMLDivElement>('.search-page-size');
if (document.querySelector<HTMLInputElement>('#use_paged_search')!.checked) {
showElem('.search-page-size');
for (const el of searchPageSizeElements) {
el.querySelector('input')?.setAttribute('required', 'required');
}
} else {
hideElem('.search-page-size');
for (const el of searchPageSizeElements) {
el.querySelector('input')?.removeAttribute('required');
}
}
}
function onOAuth2Change(applyDefaultValues: boolean) {
hideElem('.open_id_connect_auto_discovery_url, .open_id_connect_external_id_claim, .oauth2_use_custom_url');
for (const input of document.querySelectorAll<HTMLInputElement>('.open_id_connect_auto_discovery_url input[required]')) {
input.removeAttribute('required');
}
const provider = document.querySelector<HTMLInputElement>('#oauth2_provider')!.value;
switch (provider) {
case 'openidConnect':
case 'aws-cognito':
document.querySelector<HTMLInputElement>('.open_id_connect_auto_discovery_url input')!.setAttribute('required', 'required');
showElem('.open_id_connect_auto_discovery_url');
showElem('.open_id_connect_external_id_claim');
break;
default: {
const elProviderCustomUrlSettings = document.querySelector<HTMLInputElement>(`#${provider}_customURLSettings`);
if (!elProviderCustomUrlSettings) break; // some providers do not have custom URL settings
const couldChangeCustomURLs = elProviderCustomUrlSettings.getAttribute('data-available') === 'true';
const mustProvideCustomURLs = elProviderCustomUrlSettings.getAttribute('data-required') === 'true';
if (couldChangeCustomURLs) {
showElem('.oauth2_use_custom_url'); // show the checkbox
}
if (mustProvideCustomURLs) {
document.querySelector<HTMLInputElement>('#oauth2_use_custom_url')!.checked = true; // make the checkbox checked
}
break;
}
}
const supportSshPublicKey = document.querySelector<HTMLInputElement>(`#${provider}_SupportSSHPublicKey`)?.value === 'true';
toggleElem('.field.oauth2_ssh_public_key_claim_name', supportSshPublicKey);
onOAuth2UseCustomURLChange(applyDefaultValues);
}
function onOAuth2UseCustomURLChange(applyDefaultValues: boolean) {
const provider = document.querySelector<HTMLInputElement>('#oauth2_provider')!.value;
hideElem('.oauth2_use_custom_url_field');
for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) {
input.removeAttribute('required');
}
const elProviderCustomUrlSettings = document.querySelector(`#${provider}_customURLSettings`);
if (elProviderCustomUrlSettings && document.querySelector<HTMLInputElement>('#oauth2_use_custom_url')!.checked) {
for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) {
if (applyDefaultValues) {
document.querySelector<HTMLInputElement>(`#oauth2_${custom}`)!.value = document.querySelector<HTMLInputElement>(`#${provider}_${custom}`)!.value;
}
const customInput = document.querySelector(`#${provider}_${custom}`);
if (customInput?.getAttribute('data-available') === 'true') {
for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) {
input.setAttribute('required', 'required');
}
showElem(`.oauth2_${custom}`);
}
}
}
}
function onEnableLdapGroupsChange() {
const checked = document.querySelector<HTMLInputElement>('.js-ldap-group-toggle')?.checked;
toggleElem(document.querySelector('#ldap-group-options')!, checked);
}
const elAuthType = document.querySelector<HTMLInputElement>('#auth_type')!;
// New authentication
if (isNewPage) {
const onAuthTypeChange = function () {
hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi');
for (const input of document.querySelectorAll<HTMLInputElement>('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) {
input.removeAttribute('required');
}
document.querySelector<HTMLDivElement>('.binddnrequired')?.classList.remove('required');
const authType = elAuthType.value;
switch (authType) {
case '2': // LDAP
showElem('.ldap');
for (const input of document.querySelectorAll<HTMLInputElement>('.binddnrequired input, .ldap div.required:not(.dldap) input')) {
input.setAttribute('required', 'required');
}
document.querySelector('.binddnrequired')?.classList.add('required');
break;
case '3': // SMTP
showElem('.smtp');
showElem('.has-tls');
for (const input of document.querySelectorAll<HTMLInputElement>('.smtp div.required input, .has-tls')) {
input.setAttribute('required', 'required');
}
break;
case '4': // PAM
showElem('.pam');
for (const input of document.querySelectorAll<HTMLInputElement>('.pam input')) {
input.setAttribute('required', 'required');
}
break;
case '5': // LDAP
showElem('.dldap');
for (const input of document.querySelectorAll<HTMLInputElement>('.dldap div.required:not(.ldap) input')) {
input.setAttribute('required', 'required');
}
break;
case '6': // OAuth2
showElem('.oauth2');
for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) {
input.setAttribute('required', 'required');
}
onOAuth2Change(true);
break;
case '7': // SSPI
showElem('.sspi');
for (const input of document.querySelectorAll<HTMLInputElement>('.sspi div.required input')) {
input.setAttribute('required', 'required');
}
break;
}
if (authType === '2' || authType === '5') {
onSecurityProtocolChange();
onEnableLdapGroupsChange();
}
if (authType === '2') {
onUsePagedSearchChange();
}
};
elAuthType.addEventListener('change', onAuthTypeChange);
onAuthTypeChange();
document.querySelector<HTMLInputElement>('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
document.querySelector<HTMLInputElement>('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
document.querySelector<HTMLInputElement>('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
document.querySelector<HTMLInputElement>('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true));
document.querySelector('.js-ldap-group-toggle')!.addEventListener('change', onEnableLdapGroupsChange);
}
// Edit authentication
if (isEditPage) {
const authType = elAuthType.value;
if (authType === '2' || authType === '5') {
document.querySelector<HTMLInputElement>('#security_protocol')?.addEventListener('change', onSecurityProtocolChange);
document.querySelector('.js-ldap-group-toggle')!.addEventListener('change', onEnableLdapGroupsChange);
onEnableLdapGroupsChange();
if (authType === '2') {
document.querySelector<HTMLInputElement>('#use_paged_search')?.addEventListener('change', onUsePagedSearchChange);
}
} else if (authType === '6') {
document.querySelector<HTMLInputElement>('#oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true));
document.querySelector<HTMLInputElement>('#oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false));
onOAuth2Change(false);
}
}
const elAuthName = document.querySelector<HTMLInputElement>('#auth_name')!;
const onAuthNameChange = function () {
// appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash.
document.querySelector('#oauth2-callback-url')!.textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${pathEscape(elAuthName.value)}/callback`;
};
elAuthName.addEventListener('input', onAuthNameChange);
onAuthNameChange();
}
function initAdminNotice() {
const pageContent = document.querySelector('.page-content.admin.notice');
if (!pageContent) return;
const detailModal = document.querySelector<HTMLDivElement>('#detail-modal')!;
// Attach view detail modals
queryElems(pageContent, '.view-detail', (el) => el.addEventListener('click', (e) => {
e.preventDefault();
const elNoticeDesc = el.closest('tr')!.querySelector('.notice-description')!;
const elModalDesc = detailModal.querySelector('.content pre')!;
elModalDesc.textContent = elNoticeDesc.textContent;
showFomanticModal(detailModal);
}));
// Select actions
const checkboxes = document.querySelectorAll<HTMLInputElement>('.select.table .ui.checkbox input');
queryElems(pageContent, '.select.action', (el) => el.addEventListener('click', () => {
switch (el.getAttribute('data-action')) {
case 'select-all':
for (const checkbox of checkboxes) {
checkbox.checked = true;
}
break;
case 'deselect-all':
for (const checkbox of checkboxes) {
checkbox.checked = false;
}
break;
case 'inverse':
for (const checkbox of checkboxes) {
checkbox.checked = !checkbox.checked;
}
break;
}
}));
document.querySelector<HTMLButtonElement>('#delete-selection')?.addEventListener('click', async function (e) {
e.preventDefault();
this.classList.add('is-loading', 'disabled');
const data = new FormData();
for (const checkbox of checkboxes) {
if (checkbox.checked) {
data.append('ids[]', checkbox.closest('.ui.checkbox')!.getAttribute('data-id')!);
}
}
await POST(this.getAttribute('data-link')!, {data});
window.location.reload();
});
}