diff mbox series

[yocto-autobuilder2,4/4] yocto_console_view: Add release selector

Message ID 20250205-mathieu-console_view_upgrade-v1-4-fc1194355870@bootlin.com
State New
Headers show
Series Fixes for buildbot 4.2 | expand

Commit Message

Mathieu Dubois-Briand Feb. 5, 2025, 10:45 a.m. UTC
Signed-off-by: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com>
---
 yocto_console_view/src/index.ts                   |   1 +
 yocto_console_view/src/releaseselectorfield.ts    | 152 ++++++++++++++++++++++
 yocto_console_view/yocto_console_view/__init__.py |  36 +++++
 3 files changed, 189 insertions(+)
diff mbox series

Patch

diff --git a/yocto_console_view/src/index.ts b/yocto_console_view/src/index.ts
index fa0781c88e9a..de773540f6d7 100644
--- a/yocto_console_view/src/index.ts
+++ b/yocto_console_view/src/index.ts
@@ -16,3 +16,4 @@ 
 */
 
 import './views/ConsoleView/ConsoleView';
+import './releaseselectorfield';
diff --git a/yocto_console_view/src/releaseselectorfield.ts b/yocto_console_view/src/releaseselectorfield.ts
new file mode 100644
index 000000000000..a11816b21829
--- /dev/null
+++ b/yocto_console_view/src/releaseselectorfield.ts
@@ -0,0 +1,152 @@ 
+import {buildbotSetupPlugin} from "buildbot-plugin-support";
+buildbotSetupPlugin((reg) => {
+	let selectListName = null;
+	let inputRefs = null;
+	let selectors = null;
+
+	const onTransitionEndEvent = (event) => {
+		/*
+		 * We are looking for the transition showing the "forcebuild" dialog.
+		 */
+		if (!event.target.classList.contains("bb-forcebuild-modal"))
+			return
+
+		/*
+		 * Find modal-body div.
+		 */
+		const modalDialog = Array.from(event.target.children).find(e => e.classList.contains("modal-dialog"));
+		const modalContent = modalDialog ? Array.from(modalDialog.children).find(e => e.classList.contains("modal-content")) : null;
+		const modalBody = modalContent ? Array.from(modalContent.children).find(e => e.classList.contains("modal-body")) : null;
+		if (!modalBody)
+			return;
+
+		/*
+		 * Generate a map of all inputs, identifed by the field name.
+		 */
+		inputRefs = new Map();
+		document.querySelectorAll('input').forEach(input => {
+			let idparent = input;
+			while (!idparent.attributes.getNamedItem('data-bb-test-id')
+			       && idparent.parentElement != modalBody) {
+				       idparent = idparent.parentElement;
+			}
+
+			const id = idparent.attributes.getNamedItem('data-bb-test-id')
+			if (id)
+				inputRefs.set(id.value, input);
+
+			prepareInterceptor(input);
+		});
+
+		/*
+		 * Only show the pretty name in the release selector field.
+		 */
+		const releaseSelector = inputRefs.get('force-field-branchselector');
+		const releaseSelectorLabel = releaseSelector.parentNode.previousSibling;
+		const sepIdx = releaseSelectorLabel.textContent.indexOf(':');
+		releaseSelectorLabel.textContent = releaseSelectorLabel.textContent.substring(0, sepIdx);
+
+		/*
+		 * Get the name of the ReleaseSelector field div.
+		 */
+		const branchInputId = releaseSelector.attributes.getNamedItem('id').value;
+		const selectName = branchInputId.substring(0, branchInputId.lastIndexOf('-'));
+		selectListName = selectName + '-listbox';
+	}
+	window.addEventListener('transitionend', onTransitionEndEvent);
+
+	const onClick = (event) => {
+		if (selectListName) {
+			const listDiv = document.getElementById(selectListName);
+			if (listDiv) {
+				/*
+				 * The ReleaseSelector menu is shown: save
+				 * associated selectors for later and clean menu items.
+				 */
+				selectors = new Map();
+				listDiv.childNodes.forEach(div => {
+					const sepIdx = div.textContent.indexOf(':');
+					const propName = div.textContent.substring(0, sepIdx);
+					const content = div.textContent.substring(sepIdx + 1);
+					div.textContent = propName
+					selectors.set(propName, JSON.parse(content));
+				});
+			}
+		}
+
+		if (event.target.parentElement) {
+			const parentId = event.target.parentElement.attributes.getNamedItem('id');
+			if (parentId && parentId.value == selectListName) {
+				/*
+				 * One entry was clicked in the ReleaseSelector
+				 * menu: update all fields described by the
+				 * selector configuration.
+				 */
+				const selector = selectors.get(event.target.textContent);
+				if (selector) {
+					new Promise((resolve, reject) => {
+						return applySelector(selector, event.target).then(resolve);
+					});
+				}
+			}
+		}
+	}
+	window.addEventListener('click', onClick);
+
+	/*
+	 * Apply values from the selected field selector
+	 */
+	async function applySelector(selector, selectList) {
+		for (let [field, value] of Object.entries(selector)) {
+			const input = inputRefs.get('force-field-' + field);
+			if (input && input.value != value) {
+				/*
+				 * Setting value using input.value is not enough here: field
+				 * would appear modified but this value would not be used on
+				 * form submission.
+				 */
+				await setFieldValue(input, value);
+			}
+		}
+
+		const releaseSelector = inputRefs.get('force-field-branchselector');
+		releaseSelector.parentNode.previousSibling.textContent = selectList.textContent;
+		releaseSelector.focus();
+	}
+
+	/*
+	 * All code below is highly based on work from testing-library/user-event:
+	 * https://github.com/testing-library/user-event
+	 * The MIT License (MIT)
+	 * Copyright (c) 2020 Giorgio Polvara
+	 */
+	function prepareInterceptor(element) {
+		const prototypeDescriptor = Object.getOwnPropertyDescriptor(element.constructor.prototype, 'value');
+		const objectDescriptor = Object.getOwnPropertyDescriptor(element, 'value');
+		Object.defineProperty(element, 'value', {
+			objectDescriptor,
+			['set']: function(v) {
+				const realFunc = prototypeDescriptor['set'];
+				realFunc.call(this, v);
+			}
+		});
+	}
+
+	const UIValue = Symbol('Displayed value in UI');
+	async function setFieldValue(element, value) {
+		element.focus();
+		element[UIValue] = value;
+		element.value = Object.assign(new String(value), {
+			[UIValue]: true
+		});
+	}
+
+	document.addEventListener('blur', (e)=>{
+		const event = new Event('change', {bubbles: true, target: e.target, cancelable: false});
+		e.target.dispatchEvent(event);
+	}, {
+		capture: true,
+		passive: true
+	});
+
+});
diff --git a/yocto_console_view/yocto_console_view/__init__.py b/yocto_console_view/yocto_console_view/__init__.py
new file mode 100644
index 000000000000..6118ac29409b
--- /dev/null
+++ b/yocto_console_view/yocto_console_view/__init__.py
@@ -0,0 +1,36 @@ 
+# This file is part of Buildbot.  Buildbot is free software: you can
+# redistribute it and/or modify it under the terms of the GNU General Public
+# License as published by the Free Software Foundation, version 2.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Copyright Buildbot Team Members
+
+from buildbot.www.plugin import Application
+from buildbot.schedulers.forcesched import ChoiceStringParameter
+
+import json
+
+# create the interface for the setuptools entry point
+ep = Application(__package__, "Yocto Buildbot Console View plugin")
+
+class ReleaseSelector(ChoiceStringParameter):
+
+    spec_attributes = ["selectors"]
+    selectors = None
+
+    def __init__(self, name, selectors, **kw):
+        def format_choice(choice):
+            return choice + ': ' + json.dumps(selectors.get(choice, {}))
+
+        super().__init__(name, **kw)
+        self.choices = [format_choice(choice)
+                        for choice in kw.get('choices', {})]
+        self.__dict__['default'] = format_choice(self.__dict__['default'])