- Enables defining authorizations using a role-based strategy.
- Once the strategy is enabled, it can be configured via a separate page
- in the Manage Jenkins window.
-
\ No newline at end of file
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy/help_fr.html b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy/help_fr.html
index f53a2ce4..87ef8a38 100644
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy/help_fr.html
+++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleBasedAuthorizationStrategy/help_fr.html
@@ -1,30 +1,30 @@
-
-
-
-
- Permet la gestion des autorisations par une stratégie basée
- sur les rôles.
-
+
+
+
+
+ Permet la gestion des autorisations par une stratégie basée
+ sur les rôles.
+
\ No newline at end of file
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly
index c5a5fd44..53271189 100644
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly
+++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-agent-roles.jelly
@@ -1,51 +1,51 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly
index 453c6751..d11b7add 100644
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly
+++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-global-roles.jelly
@@ -1,60 +1,60 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly
index a98fb323..1bc6719a 100644
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly
+++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/assign-project-roles.jelly
@@ -1,61 +1,61 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-agent-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-agent-roles.jelly
deleted file mode 100644
index 693cce78..00000000
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-agent-roles.jelly
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%View available macros}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-global-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-global-roles.jelly
deleted file mode 100644
index 629fbb24..00000000
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-global-roles.jelly
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-global-roles_fr.properties b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-global-roles_fr.properties
deleted file mode 100644
index c3bae8bb..00000000
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-global-roles_fr.properties
+++ /dev/null
@@ -1,25 +0,0 @@
-# The MIT License
-#
-# Copyright (c) 2010-2011, Manufacture Fran\u00e7aise des Pneumatiques Michelin, Thomas Maurel
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-Role=R\u00f4le
-Role\ to\ add=R\u00f4le \u00e0 ajouter
-Add=Ajouter
\ No newline at end of file
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-project-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-project-roles.jelly
deleted file mode 100644
index 415860d5..00000000
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-project-roles.jelly
+++ /dev/null
@@ -1,105 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%View available macros}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-project-roles_fr.properties b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-project-roles_fr.properties
deleted file mode 100644
index 2df391cd..00000000
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-project-roles_fr.properties
+++ /dev/null
@@ -1,26 +0,0 @@
-# The MIT License
-#
-# Copyright (c) 2010-2011, Manufacture Fran\u00e7aise des Pneumatiques Michelin, Thomas Maurel
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-Pattern=Patron
-Role=R\u00f4le
-Role\ to\ add=R\u00f4le \u00e0 ajouter
-Add=Ajouter
\ No newline at end of file
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-roles.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-roles.jelly
index abca6837..717c7e91 100644
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-roles.jelly
+++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/manage-roles.jelly
@@ -26,227 +26,35 @@
- THE SOFTWARE.
-->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%Role}
-
-
-
-
- ${%Pattern}
-
-
-
-
- ${%Template}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${p.name}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%Add}
-
-
-
-
-
-
-
-
-
-
- ${title}
-
-
-
-
-
-
-
-
-
-
-
-
-
- "${attrs.role.pattern.toString()}"
-
-
-
-
-
-
-
-
- ${attrs.templateName}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%Permission Templates}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ ${%Add Role}
+
+
+
+ ${%Permission Templates}
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/permission-templates.jelly b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/permission-templates.jelly
index b337e974..282f0140 100644
--- a/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/permission-templates.jelly
+++ b/src/main/resources/com/michelin/cio/hudson/plugins/rolestrategy/RoleStrategyConfig/permission-templates.jelly
@@ -22,191 +22,33 @@
- THE SOFTWARE.
-->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+ ${%Add Template}
+
- ${title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%blurb}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%Name}
-
- In use
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${p.name}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${%Add}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ ${%blurb}
+
+
+
+
+
+
+
diff --git a/src/main/webapp/js/table.js b/src/main/webapp/js/table.js
index 392efd61..f70e78f2 100644
--- a/src/main/webapp/js/table.js
+++ b/src/main/webapp/js/table.js
@@ -1,143 +1,143 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2010, Manufacture Française des Pneumatiques Michelin, Thomas Maurel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-function debounce(func, timeout = 300) {
- let timer;
- return (...args) => {
- clearTimeout(timer);
- timer = setTimeout(() => { func.apply(this, args); }, timeout);
- };
-}
-
-function ignoreKeys(code) {
- switch (code) {
- case "KeyS":
- case "ArrowDown":
- case "KeyW":
- case "ArrowUp":
- case "KeyA":
- case "ArrowLeft":
- case "KeyD":
- case "ArrowRight":
- case "Enter":
- case "Escape":
- return true;
- }
- return false;
-}
-
-function getPreviousSiblings(elem, filter) {
- let sibs = [];
- while (elem = elem.previousSibling) {
- if (elem.nodeType === 3) continue; // text node
- sibs.push(elem);
- }
- return sibs;
-}
-
-class TableHighlighter {
-
- constructor(id, decalx) {
- this.table = document.getElementById(id);
- this.decalx = decalx;
- let trs = this.table.querySelectorAll('tbody tr');
- for (let row of trs){
- this.scan(row);
- }
- };
-
- scan(tr) {
- let descendants = tr.querySelectorAll('.rsp-highlight-input');
- for (let td of descendants) {
- td.addEventListener('mouseenter', this.highlight);
- td.addEventListener('mouseleave', this.highlight);
- }
- let stopNodes = tr.querySelectorAll("div.rsp-remove");
- let lastStop = stopNodes[stopNodes.length - 1];
- if (lastStop != null) {
- let td = lastStop.closest('td');
- td.addEventListener('mouseenter', this.highlightRowOnly);
- td.addEventListener('mouseleave', this.highlightRowOnly);
- }
- };
-
- highlightRowOnly = e => {
- let enable = e.type === 'mouseenter';
- let tr = e.target.closest("TR");
- if (enable) {
- tr.classList.add('highlighted');
- } else {
- tr.classList.remove('highlighted');
- }
- }
-
- highlight = e => {
- let enable = e.type === 'mouseenter';
- if (e.target.tagName === 'TD') {
- let td = e.target;
- let tr = td.parentNode;
- let trs = this.table.querySelectorAll('tr.highlight-row');
- let position = getPreviousSiblings(td).length;
-
- let p = 0;
- for (let row of trs) {
- let num = position;
- if (p==0) num = num - this.decalx;
- p++;
- let element = row.childNodes[num];
- if (enable) {
- element.classList.add('highlighted');
- } else {
- element.classList.remove('highlighted');
- }
- }
- if (enable) {
- tr.classList.add('highlighted');
- } else {
- tr.classList.remove('highlighted');
- }
- }
- };
-};
-
-var doubleEscapeHTML = function(unsafe) {
- return escapeHTML(escapeHTML(unsafe));
-};
-
-var escapeHTML = function(unsafe) {
- return unsafe.replace(/[&<>"']/g, function(m) {
- switch (m) {
- case '&':
- return '&';
- case '<':
- return '<';
- case '>':
- return '>';
- case '"':
- return '"';
- default:
- return ''';
- }
- });
-};
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2010, Manufacture Française des Pneumatiques Michelin, Thomas Maurel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+function debounce(func, timeout = 300) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => { func.apply(this, args); }, timeout);
+ };
+}
+
+function ignoreKeys(code) {
+ switch (code) {
+ case "KeyS":
+ case "ArrowDown":
+ case "KeyW":
+ case "ArrowUp":
+ case "KeyA":
+ case "ArrowLeft":
+ case "KeyD":
+ case "ArrowRight":
+ case "Enter":
+ case "Escape":
+ return true;
+ }
+ return false;
+}
+
+function getPreviousSiblings(elem, filter) {
+ let sibs = [];
+ while (elem = elem.previousSibling) {
+ if (elem.nodeType === 3) continue; // text node
+ sibs.push(elem);
+ }
+ return sibs;
+}
+
+class TableHighlighter {
+
+ constructor(id, decalx) {
+ this.table = document.getElementById(id);
+ this.decalx = decalx;
+ let trs = this.table.querySelectorAll('tbody tr');
+ for (let row of trs){
+ this.scan(row);
+ }
+ };
+
+ scan(tr) {
+ let descendants = tr.querySelectorAll('.rsp-highlight-input');
+ for (let td of descendants) {
+ td.addEventListener('mouseenter', this.highlight);
+ td.addEventListener('mouseleave', this.highlight);
+ }
+ let stopNodes = tr.querySelectorAll("div.rsp-remove");
+ let lastStop = stopNodes[stopNodes.length - 1];
+ if (lastStop != null) {
+ let td = lastStop.closest('td');
+ td.addEventListener('mouseenter', this.highlightRowOnly);
+ td.addEventListener('mouseleave', this.highlightRowOnly);
+ }
+ };
+
+ highlightRowOnly = e => {
+ let enable = e.type === 'mouseenter';
+ let tr = e.target.closest("TR");
+ if (enable) {
+ tr.classList.add('highlighted');
+ } else {
+ tr.classList.remove('highlighted');
+ }
+ }
+
+ highlight = e => {
+ let enable = e.type === 'mouseenter';
+ if (e.target.tagName === 'TD') {
+ let td = e.target;
+ let tr = td.parentNode;
+ let trs = this.table.querySelectorAll('tr.highlight-row');
+ let position = getPreviousSiblings(td).length;
+
+ let p = 0;
+ for (let row of trs) {
+ let num = position;
+ if (p==0) num = num - this.decalx;
+ p++;
+ let element = row.childNodes[num];
+ if (enable) {
+ element.classList.add('highlighted');
+ } else {
+ element.classList.remove('highlighted');
+ }
+ }
+ if (enable) {
+ tr.classList.add('highlighted');
+ } else {
+ tr.classList.remove('highlighted');
+ }
+ }
+ };
+};
+
+var doubleEscapeHTML = function(unsafe) {
+ return escapeHTML(escapeHTML(unsafe));
+};
+
+var escapeHTML = function(unsafe) {
+ return unsafe.replace(/[&<>"']/g, function(m) {
+ switch (m) {
+ case '&':
+ return '&';
+ case '<':
+ return '<';
+ case '>':
+ return '>';
+ case '"':
+ return '"';
+ default:
+ return ''';
+ }
+ });
+};
diff --git a/src/main/webapp/js/tableManage.js b/src/main/webapp/js/tableManage.js
deleted file mode 100644
index 06d88a3f..00000000
--- a/src/main/webapp/js/tableManage.js
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2022, Markus Winter
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-// number of lines required for the role filter to get enabled
-var filterLimit = 10;
-// number of lines required for the footer to get displayed
-var footerLimit = 20;
-var globalTableHighlighter;
-var newGlobalRoleTemplate;
-var projectTableHighlighter;
-var newItemRoleTemplate;
-var agentTableHighlighter;
-var newAgentRoleTemplate;
-
-
-function filterRows(filter, table) {
- for (let row of table.tBodies[0].rows) {
- let userCell = row.cells[1].textContent.toUpperCase();
- if (userCell.indexOf(filter) > -1) {
- row.style.display = "";
- } else {
- row.style.display = "none";
- }
- }
-}
-
-
-getPattern = function(row) {
- let pattern = "";
- patternEditInputs = row.getElementsByClassName("patternEdit");
- if (patternEditInputs.length > 0) {
- pattern = patternEditInputs[0].value;
- }
- return pattern;
-}
-
-
-Behaviour.specify(".row-input-filter", "RoleBasedAuthorizationStrategy", 0, function(e) {
- e.onkeyup = debounce((event) => {
- if (ignoreKeys(event.code)) {
- return;
- }
- let filter = e.value.toUpperCase();
- let table = document.getElementById(e.getAttribute("data-table-id"));
- filterRows(filter, table);
- });
-});
-
-
-Behaviour.specify("svg.icon-pencil", 'RoleBasedAuthorizationStrategy', 0, function(e) {
- e.onclick = handlePatternEdit;
-});
-
-handlePatternEdit = function() {
- let span = this.nextSibling;
- div = span.childNodes[0];
- input = span.childNodes[1];
- if (span.getAttribute("data-edit") === "false") {
- span.setAttribute("data-edit", "true");
- div.style.display = "none";
- input.type = "text";
- input.setAttribute("size", input.value.length);
- input.onkeydown = handleKey;
- const end = input.value.length;
- input.setSelectionRange(end, end);
- input.focus();
- } else {
- endPatternInput(span, false);
- this.blur();
- }
- return false;
-}
-
-handleKey = function(e) {
- let key = e.key || 0;
- if (key == "Enter" || key === "Escape") {
- e.preventDefault();
- e.stopImmediatePropagation();
- let span = e.target.parentNode;
- endPatternInput(span, key === "Escape");
- }
-};
-
-
-endPatternInput = function(span, cancel) {
- let div = span.childNodes[0];
- let input = span.childNodes[1];
- let pattern = input.value;
- let table = span.closest("TABLE");
- input.type = "hidden";
- div.style.display = "block";
- span.setAttribute("data-edit", "false");
- if (cancel) {
- input.value = div.getAttribute("data-pattern");
- } else {
- div.setAttribute("data-pattern", pattern);
- div.textContent = '"' + pattern + '"'
- let row = span.closest("TR");
- for (td of row.getElementsByClassName('permissionInput')) {
- updateTooltip(row, td, pattern);
- }
- Behaviour.applySubtree(row, true);
- }
-}
-
-
-updateTooltip = function(tr, td, pattern) {
- let tooltipTemplate = td.getAttribute("data-tooltip-template");
- let impliedByString = td.getAttribute('data-implied-by-list');
- let impliedByList = impliedByString.split(" ");
- let input = td.getElementsByTagName('INPUT')[0];
- input.disabled = false;
- let disableCheckboxes = td.getAttribute("data-disable-checkboxes");
- if (disableCheckboxes === 'true') {
- input.disabled = true;
- }
-
- let tooltip = tooltipTemplate.replace("{{PATTERNTEMPLATE}}", escapeHTML(pattern)).replace("{{GRANTBYOTHER}}", "");
- input.nextSibling.setAttribute("data-html-tooltip", tooltip);
-
- for (let permissionId of impliedByList) {
- let reference = tr.querySelector("td[data-permission-id='" + permissionId + "'] input");
- if (reference !== null) {
- if (reference.checked) {
- input.disabled = true;
- tooltip = tooltipTemplate.replace("{{PATTERNTEMPLATE}}", escapeHTML(pattern)).replace("{{GRANTBYOTHER}}", " is granted through another permission");;
- input.nextSibling.setAttribute("data-html-tooltip", tooltip); // 2.335+
- }
- }
- }
-
- if (window.registerTooltips) {
- window.registerTooltips(e.nextSibling.parentElement);
- }
-}
-
-
-Behaviour.specify(
- ".role-strategy-add-button", "RoleBasedAuthorizationStrategy", 0,
- function(elem) {
- elem.onclick = function(e) {
- let tableId = elem.getAttribute("data-table-id");
- let table = document.getElementById(tableId);
- let templateId = elem.getAttribute("data-template-id");
- let highlighter = window[elem.getAttribute("data-highlighter")];
- addButtonAction(e, templateId, table, highlighter, tableId);
- let tbody = table.tBodies[0];
- if (tbody.children.length >= filterLimit) {
- let rolefilters = document.querySelectorAll(".row-filter");
- for (let filter of rolefilters) {
- if (filter.getAttribute("data-table-id") === tableId) {
- filter.style.display = "block";
- }
- }
- }
- if (tbody.children.length >= footerLimit) {
- table.tFoot.style.display = "table-footer-group";
- }
- }
- }
-);
-
-addButtonAction = function(e, templateId, table, tableHighlighter, tableId) {
- let tbody = table.tBodies[0];
- let roleInput = document.getElementById(tableId + 'text')
- let name = roleInput.value.trim();
- if (name == "") {
- alert("Please enter a role name");
- return;
- }
- if (findElementsBySelector(tbody, "TR").find(function(n) {
- return n.getAttribute("name") == '[' + name + ']';
- }) != null) {
- alert("Entry for '" + name + "' already exists");
- return;
- }
- let pattern = "";
- let template = window[templateId].content.firstElementChild.cloneNode(true);
- let templateName = "";
-
- if (tableId !== "globalRoles") {
- let patternInput = document.getElementById(tableId + 'pattern')
- pattern = patternInput.value;
- if (pattern == "") {
- alert("Please enter a pattern");
- return;
- }
- if (tableId === "projectRoles") {
- let templateSelect = document.getElementById(tableId + 'template');
- let templateName = templateSelect.value;
- if (templateName !== '') {
- template = window[templateId + "-" + templateName].content.firstElementChild.cloneNode(true);
- }
- }
- }
-
- let copy = document.importNode(template, true);
- let child = copy.childNodes[1];
- child.textContent = name;
- if (tableId !== "globalRoles") {
- let doubleQuote = '"';
- copy.getElementsByClassName("patternAnchor")[0].textContent = doubleQuote + pattern + doubleQuote;
- copy.getElementsByClassName("patternEdit")[0].value = pattern;
- }
-
- let children = copy.childNodes
- children.forEach(function(item){
- item.outerHTML= item.outerHTML.replace(/{{ROLE}}/g, doubleEscapeHTML(name)).replace(/{{PATTERN}}/g, doubleEscapeHTML(pattern));
- });
-
- if (tableId !== "globalRoles") {
- spanElement = copy.childNodes[2].childNodes[0].childNodes[1];
- if (tableId === "projectRoles") {
- bindListenerToPattern(spanElement.childNodes[0]);
- } else {
- bindAgentListenerToPattern(spanElement.childNodes[0]);
- }
- }
-
- copy.setAttribute("name", '[' + name + ']');
- if (tableId !== "globalRoles") {
- copy.querySelector("svg.icon-pencil").onclick = handlePatternEdit;
- }
- tbody.appendChild(copy);
- tableHighlighter.scan(copy);
- Behaviour.applySubtree(copy.closest("TABLE"), true);
-}
-
-
-Behaviour.specify(".role-strategy-table .rsp-remove", 'RoleBasedAuthorizationStrategy', 0, function(e) {
- e.onclick = function() {
- let table = this.closest("TABLE");
- let tableId = table.getAttribute("id");
- let tr = this.closest("TR");
- parent = tr.parentNode;
- parent.removeChild(tr);
- if (parent.children.length < filterLimit) {
- let userfilters = document.querySelectorAll(".row-filter")
- for (let filter of userfilters) {
- if (filter.getAttribute("data-table-id") === tableId) {
- filter.style.display = "none";
- let inputs = filter.getElementsByTagName("INPUT");
- inputs[0].value = ""
- let event = new Event("keyup");
- inputs[0].dispatchEvent(event);
- }
- }
- }
- if (parent.children.length < footerLimit) {
- table.tFoot.style.display = "none";
- }
- let dirtyButton = document.getElementById("rs-dirty-indicator");
- dirtyButton.dispatchEvent(new Event('click'));
- return false;
- }
-});
-
-Behaviour.specify(".role-strategy-table td.permissionInput input", 'RoleBasedAuthorizationStrategy', 0, function(e) {
- let table = e.closest("TABLE");
- if (table.classList.contains('read-only')) {
- // if this is a read-only UI (ExtendedRead / SystemRead), do not enable checkboxes
- return;
- }
-
- let row = e.closest("TR");
- let pattern = getPattern(row);
- let td = e.closest("TD");
- updateTooltip(row, td, pattern);
- e.onchange = function() {
- Behaviour.applySubtree(row.closest("TABLE"), true);
- return true;
- };
-});
-
-
-// methods for item roles
-showMatchingProjects = function() {
- let pattern = this.textContent.substring(1, this.textContent.length - 1); // Ignore quotes for the pattern
- let maxItems = 15; // Maximum items to search for
- let url = 'strategy/getMatchingJobs';
- reqParams = {
- 'pattern': pattern,
- 'maxJobs': maxItems
- }
-
- fetch(url + toQueryString(reqParams)).then((rsp) => {
- if (rsp.ok) {
- rsp.json().then((responseJson) => {
- let matchingItems = responseJson.matchingJobs;
- let itemCount = responseJson.itemCount;
-
- if (matchingItems != null) {
- showItemsModal(matchingItems, itemCount, maxItems, pattern);
- } else {
- showErrorMessageModal();
- }
- });
- } else {
- showErrorMessageModal();
- }
- });
-}
-
-showItemsModal = function(items, itemCount, maxItems, pattern) {
- let modalTitle = '';
-
- if (items.length > 0) {
- if (itemCount > items.length) {
- modalTitle += 'First ' + maxItems + ' items (out of ' + itemCount + ') matching';
- } else {
- modalTitle += 'Items matching';
- }
- } else {
- modalTitle = 'No items found matching';
- }
- modalTitle += ' "' + pattern + '"';
- showModal(modalTitle, items)
-}
-
-showErrorMessageModal = function() {
- dialog.alert('Unable to fetch matching Jobs.');
-}
-
-bindListenerToPattern = function(elem) {
- elem.addEventListener('click', showMatchingProjects);
-}
-
-
-// methods for agent roles
-showMatchingAgents = function() {
- let pattern = this.textContent.substring(1, this.textContent.length - 1); // Ignore quotes for the pattern
- let maxAgents = 10; // Maximum agents to search for
- let url = 'strategy/getMatchingAgents';
- reqParams = {
- 'pattern': pattern,
- 'maxAgents': maxAgents
- }
-
- fetch(url + toQueryString(reqParams)).then((rsp) => {
- if (rsp.ok) {
- rsp.json().then((responseJson) => {
- let matchingAgents = responseJson.matchingAgents;
- let agentCount = responseJson.itemCount;
-
- if (matchingAgents != null) {
- showAgentsModal(matchingAgents, agentCount, maxAgents, pattern);
- } else {
- showAgentErrorMessageModal();
- }
- });
- } else {
- showAgentErrorMessageModal();
- }
- });
-}
-
-showAgentsModal = function(agents, agentCount, maxAgents, pattern) {
- let modalTitle = '';
- if (agents.length > 0) {
- if (agentCount > agents.length) {
- modalTitle += 'First ' + maxAgents + ' agents (out of ' + agentCount + ') matching';
- } else {
- modalTitle += 'Agents matching';
- }
- } else {
- modalTitle += 'No Agents found matching';
- }
- modalTitle += ' "' + pattern + '"';
- showModal(modalTitle, agents)
-}
-
-showModal = function(title, itemlist) {
- messageElement=document.createElement("div");
- for (let item of itemlist) {
- line = document.createTextNode("- " + item);
- messageElement.appendChild(line);
- messageElement.appendChild(document.createElement("br"));
- }
- dialog.modal(messageElement, {title: title});
-}
-
-showAgentErrorMessageModal = function() {
- dialogalert('Unable to fetch matching Agents.');
-}
-
-bindAgentListenerToPattern = function(elem) {
- elem.addEventListener('click', showMatchingAgents);
-}
-
-document.addEventListener('DOMContentLoaded', function() {
-
- // global roles initialization
- const globalTable = document.getElementById("globalRoles");
- if (globalTable) {
- const readOnly = globalTable.classList.contains("read-only");
-
- let globalRoleInputFilter = document.getElementById('globalRoleInputFilter');
- if (globalRoleInputFilter && parseInt(globalRoleInputFilter.getAttribute("data-initial-size")) >= filterLimit) {
- globalRoleInputFilter.style.display = "block"
- }
- newGlobalRoleTemplate = document.getElementById('newGlobalRoleTemplate');
-
- globalTableHighlighter = new TableHighlighter('globalRoles', readOnly ? 1 : 2);
- }
-
- // item roles initialization
- const projectRolesTable = document.getElementById('projectRoles');
- if (projectRolesTable) {
- const readOnly = projectRolesTable.classList.contains("read-only");
-
- let itemRoleInputFilter = document.getElementById('itemRoleInputFilter');
- if (itemRoleInputFilter && parseInt(itemRoleInputFilter.getAttribute("data-initial-size")) >= filterLimit ) {
- itemRoleInputFilter.style.display = "block"
- }
-
- newItemRoleTemplate = document.getElementById('newItemRoleTemplate');
-
- projectTableHighlighter = new TableHighlighter('projectRoles', readOnly ? 3 : 4);
-
- // Show jobs matching a pattern on click
- let itemPatterns = projectRolesTable.getElementsByClassName('patternAnchor');
- for (let pattern of itemPatterns) {
- bindListenerToPattern(pattern);
- }
- }
-
- // agent roles initialization
- const agentRolesTable = document.getElementById('agentRoles');
- if (agentRolesTable) {
- const readOnly = agentRolesTable.classList.contains("read-only");
-
- newAgentRoleTemplate = document.getElementById('newAgentRoleTemplate');
-
- agentTableHighlighter = new TableHighlighter('agentRoles', readOnly ? 2 : 3);
- // Show agents matching a pattern on click
- let agentPatterns = agentRolesTable.getElementsByClassName('patternAnchor');
- for (let pattern of agentPatterns) {
- bindAgentListenerToPattern(pattern);
- }
- }
-});
\ No newline at end of file
diff --git a/src/main/webapp/js/tableTemplates.js b/src/main/webapp/js/tableTemplates.js
deleted file mode 100644
index 9377cf93..00000000
--- a/src/main/webapp/js/tableTemplates.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2022, Markus Winter
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-// number of lines required for the role filter to get enabled
-var filterLimit = 10;
-// number of lines required for the footer to get displayed
-var footerLimit = 20;
-
-
-function filterRows(filter, table) {
- for (let row of table.tBodies[0].rows) {
- let userCell = row.cells[1].textContent.toUpperCase();
- if (userCell.indexOf(filter) > -1) {
- row.style.display = "";
- } else {
- row.style.display = "none";
- }
- }
-}
-
-updateTooltip = function(tr, td) {
- let tooltipTemplate = td.getAttribute("data-tooltip-template");
- let impliedByString = td.getAttribute('data-implied-by-list');
- let impliedByList = impliedByString.split(" ");
- let input = td.getElementsByTagName('INPUT')[0];
- input.disabled = false;
- let tooltip = tooltipTemplate.replace("{{GRANTBYOTHER}}", "");
- input.nextSibling.setAttribute("data-html-tooltip", tooltip);
-
- for (let permissionId of impliedByList) {
- let reference = tr.querySelector("td[data-permission-id='" + permissionId + "'] input");
- if (reference !== null) {
- if (reference.checked) {
- input.disabled = true;
- tooltip = tooltipTemplate.replace("{{GRANTBYOTHER}}", " is granted through another permission");;
- input.nextSibling.setAttribute('data-html-tooltip', tooltip); // 2.335+
- }
- }
- }
-}
-
-Behaviour.specify(".row-input-filter", "RoleBasedAuthorizationStrategy", 0, function(e) {
- e.onkeyup = function() {
- let filter = e.value.toUpperCase();
- let table = document.getElementById(e.getAttribute("data-table-id"));
- filterRows(filter, table);
- }
-});
-
-Behaviour.specify(
- ".template-add-button", "RoleBasedAuthorizationStrategy", 0, function(elem) {
- elem.onclick = function(e) {
- let tableId = elem.getAttribute("data-table-id");
- let table = document.getElementById(tableId);
- let templateId = elem.getAttribute("data-template-id");
- let template = window[templateId].content.firstElementChild.cloneNode(true);
- let highlighter = window[elem.getAttribute("data-highlighter")];
- addButtonAction(e, template, table, highlighter, tableId);
- let tbody = table.tBodies[0];
- if (tbody.children.length >= filterLimit) {
- let rolefilters = document.querySelectorAll(".row-filter");
- for (let filter of rolefilters) {
- if (filter.getAttribute("data-table-id") === tableId) {
- filter.style.display = "block";
- }
- }
- }
- if (tbody.children.length >= footerLimit) {
- table.tFoot.style.display = "table-footer-group";
- }
- }
- }
-);
-
-addButtonAction = function (e, template, table, tableHighlighter, tableId) {
- let dataReference = e.target;
- let tbody = table.tBodies[0];
-
- dialog.prompt(dataReference.getAttribute('data-prompt')).then((name) => {
- name = name.trim();
- if (findElementsBySelector(tbody,"TR").find(function(n){return n.getAttribute("name")=='['+name+']';})!=null) {
- dialog.alert("Entry for '"+name+"' already exists");
- return;
- }
-
- let copy = document.importNode(template,true);
- let child = copy.childNodes[1];
- child.textContent = escapeHTML(name);
-
- let children = copy.getElementsByClassName("permissionInput");
- for (let child of children) {
- if (child.hasAttribute('data-tooltip-template')) {
- child.setAttribute("data-tooltip-template", child.getAttribute("data-tooltip-template").replace(/{{TEMPLATE}}/g, doubleEscapeHTML(name)));
- }
- }
-
- if (tableId !== "permissionTemplates") {
- spanElement = copy.childNodes[2].childNodes[0].childNodes[1];
- }
-
- copy.setAttribute("name",'['+name+']');
- tbody.appendChild(copy);
- if (tableHighlighter != null) {
- tableHighlighter.scan(copy);
- }
- Behaviour.applySubtree(copy.closest("TABLE"), true);
- })
-}
-
-function deleteTemplate(button) {
- let table = button.closest("TABLE");
- let tableId = table.getAttribute("id");
- let tr = button.closest("TR");
- parent = tr.parentNode;
- parent.removeChild(tr);
- if (parent.children.length < filterLimit) {
- let userfilters = document.querySelectorAll(".row-filter")
- for (let filter of userfilters) {
- if (filter.getAttribute("data-table-id") === tableId) {
- filter.style.display = "none";
- let inputs = filter.getElementsByTagName("INPUT");
- inputs[0].value=""
- let event = new Event("keyup");
- inputs[0].dispatchEvent(event);
- }
- }
- }
- if (parent.children.length < footerLimit) {
- table.tFoot.style.display = "none";
- }
-
-}
-
-Behaviour.specify(".role-strategy-table .rsp-remove", 'RoleBasedAuthorizationStrategy', 0, function(e) {
- e.onclick = function() {
- let inuse = e.getAttribute("data-is-used") === "true";
- if (inuse) {
- dialog.confirm("This template is used. Are you sure you want to delete it?").then(() => {
- deleteTemplate(e);
- });
- return;
- }
- deleteTemplate(e);
- return false;
- }
-});
-
-Behaviour.specify(".role-strategy-table td.permissionInput input", 'RoleBasedAuthorizationStrategy', 0, function(e) {
- let table = e.closest("TABLE");
- if (table.classList.contains('read-only')) {
- // if this is a read-only UI (ExtendedRead / SystemRead), do not enable checkboxes
- return;
- }
- let row = e.closest("TR");
- let td = e.closest("TD");
- updateTooltip(row, td);
- e.onchange = function() {
- Behaviour.applySubtree(row.closest("TABLE"),true);
- return true;
- };
-});
-
-
-var templateTableHighlighter;
-var newPermissionTemplate;
-
-document.addEventListener('DOMContentLoaded', function() {
- let permissionInputFilter = document.getElementById('permissionInputFilter');
- if (parseInt(permissionInputFilter.getAttribute("data-initial-size")) >= filterLimit) {
- permissionInputFilter.style.display = "block"
- }
- const table = document.getElementById("permissionTemplates");
- const readOnly = table.classList.contains("read-only");
- newPermissionTemplate = document.getElementById('newPermissionTemplate');
- templateTableHighlighter = new TableHighlighter('permissionTemplates', readOnly ? 2 : 3);
-});
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/ManageRolesUITest.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/ManageRolesUITest.java
new file mode 100644
index 00000000..d598d34a
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/ManageRolesUITest.java
@@ -0,0 +1,144 @@
+package org.jenkinsci.plugins.rolestrategy.playwright;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import org.jenkinsci.plugins.rolestrategy.playwright.config.PlaywrightConfig;
+import org.jenkinsci.plugins.rolestrategy.playwright.helpers.UITestHelper;
+import org.jenkinsci.plugins.rolestrategy.playwright.pages.ManageRolesPage;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+/**
+ * End-to-end UI tests for the React-based Manage Roles page.
+ */
+@WithJenkins
+@UsePlaywright(PlaywrightConfig.class)
+@DisplayName("Manage Roles page")
+class ManageRolesUITest {
+
+ private static final String GLOBAL = "globalRoles";
+ private static final String PROJECT = "projectRoles";
+ private static final String AGENT = "slaveRoles";
+
+ private static final String OVERALL_READ = "hudson.model.Hudson.Read";
+ private static final String ITEM_READ = "hudson.model.Item.Read";
+ private static final String ITEM_BUILD = "hudson.model.Item.Build";
+ private static final String COMPUTER_BUILD = "hudson.model.Computer.Build";
+
+ private ManageRolesPage open(JenkinsRule j, Page page) throws Exception {
+ UITestHelper.setupRbasWithRoles(j);
+ String baseUrl = j.jenkins.getRootUrl();
+ UITestHelper.loginAsAdmin(page, baseUrl);
+ return new ManageRolesPage(page, baseUrl).goTo();
+ }
+
+ @Test
+ @DisplayName("lists the seeded roles in their scope sections with patterns")
+ void listsSeededRoles(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .hasRoleCard(GLOBAL, "auditor")
+ .hasRoleCard(PROJECT, "developers")
+ .roleCardHasPattern(PROJECT, "developers", "dev-.*")
+ .hasRoleCard(AGENT, "operators")
+ .roleCardHasPattern(AGENT, "operators", "node-.*");
+ }
+
+ @Test
+ @DisplayName("adds a global role through the dialog")
+ void addGlobalRole(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickAddRole()
+ .waitForDialog()
+ .dialogSelectScope(GLOBAL)
+ .dialogSetName("viewer")
+ .dialogTogglePermission(OVERALL_READ)
+ .submitDialog()
+ .hasRoleCard(GLOBAL, "viewer")
+ .roleCardHasSummary(GLOBAL, "viewer", "Read");
+ }
+
+ @Test
+ @DisplayName("adds an item role with a pattern through the dialog")
+ void addItemRoleWithPattern(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickAddRole()
+ .waitForDialog()
+ .dialogSelectScope(PROJECT)
+ .dialogSetName("qa")
+ .dialogSetPattern("qa-.*")
+ .dialogTogglePermission(ITEM_READ)
+ .submitDialog()
+ .hasRoleCard(PROJECT, "qa")
+ .roleCardHasPattern(PROJECT, "qa", "qa-.*")
+ .roleCardHasSummary(PROJECT, "qa", "Read");
+ }
+
+ @Test
+ @DisplayName("adds an item role whose permissions are pre-filled from a template")
+ void addItemRoleFromTemplate(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickAddRole()
+ .waitForDialog()
+ .dialogSelectScope(PROJECT)
+ .dialogSetName("reviewers")
+ .dialogSetPattern("review-.*")
+ .dialogSelectTemplate("review-template")
+ .submitDialog()
+ .hasRoleCard(PROJECT, "reviewers")
+ .roleCardHasTemplateBadge(PROJECT, "reviewers", "review-template")
+ .roleCardHasSummary(PROJECT, "reviewers", "Read");
+ }
+
+ @Test
+ @DisplayName("adds an agent role with a pattern through the dialog")
+ void addAgentRole(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickAddRole()
+ .waitForDialog()
+ .dialogSelectScope(AGENT)
+ .dialogSetName("linux-nodes")
+ .dialogSetPattern("linux-.*")
+ .dialogTogglePermission(COMPUTER_BUILD)
+ .submitDialog()
+ .hasRoleCard(AGENT, "linux-nodes")
+ .roleCardHasPattern(AGENT, "linux-nodes", "linux-.*");
+ }
+
+ @Test
+ @DisplayName("edits an existing role to grant an additional permission")
+ void editRoleAddsPermission(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickEditRole(PROJECT, "developers")
+ .waitForDialog()
+ .dialogTogglePermission("hudson.model.Item.Configure")
+ .submitDialog()
+ .roleCardHasSummary(PROJECT, "developers", "Configure");
+ }
+
+ @Test
+ @DisplayName("keeps the role when deletion is cancelled and removes it when confirmed")
+ void deleteRoleCancelThenConfirm(JenkinsRule j, Page page) throws Exception {
+ ManageRolesPage roles = open(j, page);
+
+ roles.clickDeleteRole(AGENT, "operators")
+ .confirmDialogContainsText("operators")
+ .dismissConfirmDialog()
+ .hasRoleCard(AGENT, "operators");
+
+ roles.clickDeleteRole(AGENT, "operators")
+ .acceptConfirmDialog()
+ .hasNoRoleCard(AGENT, "operators");
+ }
+
+ @Test
+ @DisplayName("filters the visible roles as the user types in the search box")
+ void searchFiltersRoles(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .search("developers")
+ .hasRoleCard(PROJECT, "developers")
+ .hasNoRoleCard(GLOBAL, "auditor")
+ .hasNoRoleCard(AGENT, "operators");
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/PermissionTemplatesUITest.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/PermissionTemplatesUITest.java
new file mode 100644
index 00000000..b5b8628c
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/PermissionTemplatesUITest.java
@@ -0,0 +1,131 @@
+package org.jenkinsci.plugins.rolestrategy.playwright;
+
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.junit.UsePlaywright;
+import org.jenkinsci.plugins.rolestrategy.playwright.config.PlaywrightConfig;
+import org.jenkinsci.plugins.rolestrategy.playwright.helpers.UITestHelper;
+import org.jenkinsci.plugins.rolestrategy.playwright.pages.PermissionTemplatesPage;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+/**
+ * End-to-end UI tests for the React-based Permission Templates page.
+ */
+@WithJenkins
+@UsePlaywright(PlaywrightConfig.class)
+@DisplayName("Permission Templates page")
+class PermissionTemplatesUITest {
+
+ private static final String ITEM_READ = "hudson.model.Item.Read";
+ private static final String ITEM_BUILD = "hudson.model.Item.Build";
+ private static final String ITEM_CONFIGURE = "hudson.model.Item.Configure";
+
+ private PermissionTemplatesPage open(JenkinsRule j, Page page) throws Exception {
+ UITestHelper.setupRbasWithTemplates(j);
+ String baseUrl = j.jenkins.getRootUrl();
+ UITestHelper.loginAsAdmin(page, baseUrl);
+ return new PermissionTemplatesPage(page, baseUrl).goTo();
+ }
+
+ @Test
+ @DisplayName("lists templates seeded in the configuration with their permission summaries")
+ void listsSeededTemplates(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .hasTemplateCard("dev-template")
+ .templateCardHasSummary("dev-template", "Build")
+ .templateCardHasSummary("dev-template", "Read")
+ .hasTemplateCard("empty-template");
+ }
+
+ @Test
+ @DisplayName("shows an 'In use' badge and disables deletion for a template used by a role")
+ void inUseTemplateHasBadgeAndDeleteDisabled(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .templateCardHasInUseBadge("dev-template")
+ .deleteButtonDisabled("dev-template");
+ }
+
+ @Test
+ @DisplayName("does not allow expanding a template that has no permissions")
+ void emptyTemplateIsNotExpandable(JenkinsRule j, Page page) throws Exception {
+ open(j, page).templateCardNotExpandable("empty-template");
+ }
+
+ @Test
+ @DisplayName("adds a new template through the dialog")
+ void addTemplate(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickAddTemplate()
+ .waitForDialog()
+ .dialogSetName("qa-template")
+ .dialogTogglePermission(ITEM_CONFIGURE)
+ .submitDialog()
+ .hasTemplateCard("qa-template")
+ .templateCardHasSummary("qa-template", "Configure");
+ }
+
+ @Test
+ @DisplayName("submits the add dialog when Enter is pressed in the name field")
+ void addTemplateWithEnterKey(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .clickAddTemplate()
+ .waitForDialog()
+ .dialogSetName("enter-template")
+ .dialogTogglePermission(ITEM_READ)
+ .submitDialogWithEnter()
+ .hasTemplateCard("enter-template");
+ }
+
+ @Test
+ @DisplayName("edits an existing template to grant an additional permission")
+ void editTemplateAddsPermission(JenkinsRule j, Page page) throws Exception {
+ PermissionTemplatesPage templates = open(j, page);
+
+ // Seed a fresh, unused template so it can be edited and deleted freely.
+ templates.clickAddTemplate()
+ .waitForDialog()
+ .dialogSetName("edit-me")
+ .dialogTogglePermission(ITEM_READ)
+ .submitDialog()
+ .templateCardHasSummary("edit-me", "Read");
+
+ templates.clickEditTemplate("edit-me")
+ .waitForDialog()
+ .dialogTogglePermission(ITEM_BUILD)
+ .submitDialog()
+ .templateCardHasSummary("edit-me", "Build");
+ }
+
+ @Test
+ @DisplayName("keeps the template when deletion is cancelled and removes it when confirmed")
+ void deleteTemplateCancelKeepsItThenConfirmRemovesIt(JenkinsRule j, Page page) throws Exception {
+ PermissionTemplatesPage templates = open(j, page);
+
+ templates.clickAddTemplate()
+ .waitForDialog()
+ .dialogSetName("disposable")
+ .dialogTogglePermission(ITEM_READ)
+ .submitDialog()
+ .hasTemplateCard("disposable");
+
+ templates.clickDeleteTemplate("disposable")
+ .confirmDialogContainsText("disposable")
+ .dismissConfirmDialog()
+ .hasTemplateCard("disposable");
+
+ templates.clickDeleteTemplate("disposable")
+ .acceptConfirmDialog()
+ .hasNoTemplateCard("disposable");
+ }
+
+ @Test
+ @DisplayName("filters the visible templates as the user types in the search box")
+ void searchFiltersTemplates(JenkinsRule j, Page page) throws Exception {
+ open(j, page)
+ .search("dev")
+ .hasTemplateCard("dev-template")
+ .hasNoTemplateCard("empty-template");
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/config/PlaywrightConfig.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/config/PlaywrightConfig.java
new file mode 100644
index 00000000..834434ef
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/config/PlaywrightConfig.java
@@ -0,0 +1,17 @@
+package org.jenkinsci.plugins.rolestrategy.playwright.config;
+
+import com.microsoft.playwright.junit.Options;
+import com.microsoft.playwright.junit.OptionsFactory;
+
+/**
+ * Playwright options for the role-strategy UI tests. Runs Chromium headless by
+ * default; pass {@code -Dplaywright.headless=false} to watch the browser.
+ */
+public class PlaywrightConfig implements OptionsFactory {
+
+ @Override
+ public Options getOptions() {
+ boolean headless = Boolean.parseBoolean(System.getProperty("playwright.headless", "true"));
+ return new Options().setBrowserName("chromium").setHeadless(headless);
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/helpers/UITestHelper.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/helpers/UITestHelper.java
new file mode 100644
index 00000000..9a856911
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/helpers/UITestHelper.java
@@ -0,0 +1,85 @@
+package org.jenkinsci.plugins.rolestrategy.playwright.helpers;
+
+import com.michelin.cio.hudson.plugins.rolestrategy.RoleBasedAuthorizationStrategy;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.AriaRole;
+import java.io.IOException;
+import org.jvnet.hudson.test.JenkinsRule;
+
+/**
+ * Shared setup for the Playwright UI tests.
+ */
+public final class UITestHelper {
+
+ private UITestHelper() {}
+
+ /** Log in to Jenkins as admin via the Playwright browser. */
+ public static void loginAsAdmin(Page page, String baseUrl) {
+ page.navigate(baseUrl + "login");
+ page.locator("input[name='j_username']").fill("admin");
+ page.locator("input[name='j_password']").fill("admin");
+ page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
+ page.waitForURL(baseUrl + "**");
+ }
+
+ /**
+ * Configure Jenkins with a dummy security realm and a role-based authorization strategy.
+ * Seeded with permission templates, the templates UI test relies on:
+ *
+ *
+ * {@code dev-template} — Item Read + Build, referenced by an item role so it is "in use"
+ * {@code empty-template} — no permissions (non-expandable card)
+ *
+ */
+ public static RoleBasedAuthorizationStrategy setupRbasWithTemplates(JenkinsRule j) throws IOException {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ RoleBasedAuthorizationStrategy rbas = new RoleBasedAuthorizationStrategy();
+ j.jenkins.setAuthorizationStrategy(rbas);
+
+ rbas.doAddRole("globalRoles", "admin",
+ "hudson.model.Hudson.Administer", "false", "", "");
+ rbas.doAssignUserRole("globalRoles", "admin", "admin");
+
+ rbas.doAddTemplate("dev-template",
+ "hudson.model.Item.Read,hudson.model.Item.Build", false);
+ rbas.doAddTemplate("empty-template", "", false);
+
+ // An item role referencing dev-template marks it as "in use".
+ rbas.doAddRole("projectRoles", "templated-role", "", "false", "tmpl-.*", "dev-template");
+
+ return rbas;
+ }
+
+ /**
+ * Configure Jenkins with a dummy security realm and a role-based authorization strategy.
+ * Seeded with one role of each scope, the Manage Roles UI test lists:
+ *
+ *
+ * global {@code admin} (Administer), assigned to the admin user
+ * global {@code auditor} (Overall/Read)
+ * item {@code developers} (pattern {@code dev-.*}, Item Read + Build)
+ * agent {@code operators} (pattern {@code node-.*}, Computer Build)
+ * {@code review-template} (Item Read), available in the Add dialog's template dropdown
+ *
+ */
+ public static RoleBasedAuthorizationStrategy setupRbasWithRoles(JenkinsRule j) throws IOException {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ RoleBasedAuthorizationStrategy rbas = new RoleBasedAuthorizationStrategy();
+ j.jenkins.setAuthorizationStrategy(rbas);
+
+ rbas.doAddRole("globalRoles", "admin",
+ "hudson.model.Hudson.Administer", "false", "", "");
+ rbas.doAssignUserRole("globalRoles", "admin", "admin");
+
+ rbas.doAddRole("globalRoles", "auditor",
+ "hudson.model.Hudson.Read", "false", "", "");
+ rbas.doAddRole("projectRoles", "developers",
+ "hudson.model.Item.Read,hudson.model.Item.Build", "false", "dev-.*", "");
+ rbas.doAddRole("slaveRoles", "operators",
+ "hudson.model.Computer.Build", "false", "node-.*", "");
+
+ rbas.doAddTemplate("review-template", "hudson.model.Item.Read", false);
+
+ return rbas;
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/ManageRolesPage.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/ManageRolesPage.java
new file mode 100644
index 00000000..7864080a
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/ManageRolesPage.java
@@ -0,0 +1,123 @@
+package org.jenkinsci.plugins.rolestrategy.playwright.pages;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.AriaRole;
+
+/**
+ * Page object for the Manage Roles page. Each role scope renders as a
+ * {@code section.rsp-container[data-role-type]} holding {@code .rsp-card} roles
+ * identified by their {@code .rsp-card__name} text; cards are scoped to their
+ * section because a name may be reused across scopes. Filtering removes
+ * non-matching cards from the DOM, so a hidden card asserts as absent. Shared
+ * modal handling lives in {@link RoleStrategyPage}.
+ */
+public class ManageRolesPage extends RoleStrategyPage {
+
+ public ManageRolesPage(Page page, String baseUrl) {
+ super(page, baseUrl + "manage/role-strategy/manage-roles");
+ }
+
+ @Override
+ protected void waitForLoaded() {
+ assertThat(page.getByRole(AriaRole.HEADING,
+ new Page.GetByRoleOptions().setName("Roles"))).isVisible();
+ }
+
+ // --- Card assertions ---
+
+ public ManageRolesPage hasRoleCard(String type, String name) {
+ assertThat(card(type, name)).isVisible();
+ return this;
+ }
+
+ public ManageRolesPage hasNoRoleCard(String type, String name) {
+ assertThat(card(type, name)).hasCount(0);
+ return this;
+ }
+
+ public ManageRolesPage roleCardHasPattern(String type, String name, String expected) {
+ assertThat(card(type, name).locator(".rsp-card__pattern")).containsText(expected);
+ return this;
+ }
+
+ public ManageRolesPage roleCardHasSummary(String type, String name, String expected) {
+ assertThat(card(type, name).locator(".rsp-card__summary")).containsText(expected);
+ return this;
+ }
+
+ public ManageRolesPage roleCardHasTemplateBadge(String type, String name, String templateName) {
+ assertThat(card(type, name).locator(".rsp-card__template-badge")).containsText(templateName);
+ return this;
+ }
+
+ // --- Actions ---
+
+ public ManageRolesPage clickAddRole() {
+ page.locator("#rsp-add-role-btn").click();
+ return this;
+ }
+
+ public ManageRolesPage clickEditRole(String type, String name) {
+ card(type, name).locator(".rsp-card__actions button").first().click();
+ return this;
+ }
+
+ public ManageRolesPage clickDeleteRole(String type, String name) {
+ card(type, name).locator(".rsp-card__actions button").last().click();
+ return this;
+ }
+
+ public ManageRolesPage search(String query) {
+ page.getByPlaceholder("Search roles").fill(query);
+ return this;
+ }
+
+ // --- Add/Edit dialog ---
+
+ public ManageRolesPage waitForDialog() {
+ assertThat(dialog()).isVisible();
+ dialog().locator(".rsp-assign-dialog__role-item").first()
+ .waitFor(new Locator.WaitForOptions().setTimeout(60000));
+ return this;
+ }
+
+ public ManageRolesPage dialogSelectScope(String type) {
+ dialog().locator(".jenkins-radio:has(input[value='" + type + "']) label").click();
+ return this;
+ }
+
+ public ManageRolesPage dialogSetName(String name) {
+ dialog().locator("#rsp-add-role-name").fill(name);
+ return this;
+ }
+
+ public ManageRolesPage dialogSetPattern(String pattern) {
+ dialog().locator("#rsp-add-role-pattern").fill(pattern);
+ return this;
+ }
+
+ public ManageRolesPage dialogSelectTemplate(String templateName) {
+ dialog().locator("#rsp-add-role-template").selectOption(templateName);
+ return this;
+ }
+
+ public ManageRolesPage dialogTogglePermission(String permissionId) {
+ dialog().locator(".rsp-assign-dialog__role-item[data-permission-id='" + permissionId + "'] label")
+ .click();
+ return this;
+ }
+
+ // --- Locators ---
+
+ private Locator section(String type) {
+ return page.locator(".rsp-container[data-role-type='" + type + "']");
+ }
+
+ private Locator card(String type, String name) {
+ return section(type)
+ .locator(".rsp-card:has(.rsp-card__name:text-is(\"" + name + "\"))");
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/PermissionTemplatesPage.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/PermissionTemplatesPage.java
new file mode 100644
index 00000000..87d4d329
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/PermissionTemplatesPage.java
@@ -0,0 +1,122 @@
+package org.jenkinsci.plugins.rolestrategy.playwright.pages;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.options.AriaRole;
+
+/**
+ * Page object for the Permission Templates page. Templates render as
+ * {@code .rsp-card} elements identified by their {@code .rsp-card__name} text;
+ * filtering removes non-matching cards from the DOM, so a hidden card asserts as
+ * absent. Shared modal handling lives in {@link RoleStrategyPage}.
+ */
+public class PermissionTemplatesPage extends RoleStrategyPage {
+
+ public PermissionTemplatesPage(Page page, String baseUrl) {
+ super(page, baseUrl + "manage/role-strategy/permission-templates");
+ }
+
+ @Override
+ protected void waitForLoaded() {
+ assertThat(page.getByRole(AriaRole.HEADING,
+ new Page.GetByRoleOptions().setName("Permission Templates"))).isVisible();
+ }
+
+ // --- Card assertions ---
+
+ public PermissionTemplatesPage hasTemplateCard(String name) {
+ assertThat(card(name)).isVisible();
+ return this;
+ }
+
+ public PermissionTemplatesPage hasNoTemplateCard(String name) {
+ assertThat(card(name)).hasCount(0);
+ return this;
+ }
+
+ public PermissionTemplatesPage templateCardHasSummary(String name, String expected) {
+ assertThat(card(name).locator(".rsp-card__summary")).containsText(expected);
+ return this;
+ }
+
+ public PermissionTemplatesPage templateCardHasInUseBadge(String name) {
+ assertThat(card(name).locator(".rsp-card__template-badge")).containsText("In use");
+ return this;
+ }
+
+ public PermissionTemplatesPage templateCardNotExpandable(String name) {
+ // The toggle's slot stays in place to keep actions aligned, but it carries
+ // no chevron and the header is not a button when there is nothing to expand.
+ assertThat(card(name).locator(".rsp-card__toggle svg")).hasCount(0);
+ assertThat(card(name).locator(".rsp-card__header"))
+ .not()
+ .hasAttribute("role", "button");
+ return this;
+ }
+
+ public PermissionTemplatesPage deleteButtonDisabled(String name) {
+ assertThat(deleteButton(name)).isDisabled();
+ return this;
+ }
+
+ // --- Actions ---
+
+ public PermissionTemplatesPage clickAddTemplate() {
+ page.locator("#rsp-add-template-btn").click();
+ return this;
+ }
+
+ public PermissionTemplatesPage clickEditTemplate(String name) {
+ card(name).locator(".rsp-card__actions button").first().click();
+ return this;
+ }
+
+ public PermissionTemplatesPage clickDeleteTemplate(String name) {
+ deleteButton(name).click();
+ return this;
+ }
+
+ public PermissionTemplatesPage search(String query) {
+ page.getByPlaceholder("Search templates").fill(query);
+ return this;
+ }
+
+ // --- Add/Edit dialog fields (template-specific) ---
+
+ public PermissionTemplatesPage waitForDialog() {
+ assertThat(dialog()).isVisible();
+ dialog().locator(".rsp-assign-dialog__role-item").first()
+ .waitFor(new Locator.WaitForOptions().setTimeout(60000));
+ return this;
+ }
+
+ public PermissionTemplatesPage dialogSetName(String name) {
+ dialog().locator("#rsp-template-name").fill(name);
+ return this;
+ }
+
+ /** Submit the dialog by pressing Enter from the name field. */
+ public PermissionTemplatesPage submitDialogWithEnter() {
+ dialog().locator("#rsp-template-name").press("Enter");
+ assertThat(dialog()).hasCount(0);
+ return this;
+ }
+
+ public PermissionTemplatesPage dialogTogglePermission(String permissionId) {
+ dialog().locator(".rsp-assign-dialog__role-item[data-permission-id='" + permissionId + "'] label")
+ .click();
+ return this;
+ }
+
+ // --- Locators ---
+
+ private Locator card(String name) {
+ return page.locator(".rsp-card:has(.rsp-card__name:text-is(\"" + name + "\"))");
+ }
+
+ private Locator deleteButton(String name) {
+ return card(name).locator(".rsp-card__actions button").last();
+ }
+}
diff --git a/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/RoleStrategyPage.java b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/RoleStrategyPage.java
new file mode 100644
index 00000000..a6b3ac52
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/rolestrategy/playwright/pages/RoleStrategyPage.java
@@ -0,0 +1,82 @@
+package org.jenkinsci.plugins.rolestrategy.playwright.pages;
+
+import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
+
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+
+/**
+ * Base page object for the React role-strategy pages, holding the bits that are
+ * common across them: navigation and the two kinds of modal — the React add/edit
+ * {@code dialog.rsp-dialog} and Jenkins core's confirm dialog (a plain
+ * {@code dialog.jenkins-dialog} opened by {@code dialog.confirm}).
+ *
+ * Uses the self-type generic so the shared fluent methods return the concrete
+ * page type.
+ *
+ * @param the concrete page object type
+ */
+abstract class RoleStrategyPage> {
+
+ protected final Page page;
+ private final String pageUrl;
+
+ protected RoleStrategyPage(Page page, String pageUrl) {
+ this.page = page;
+ this.pageUrl = pageUrl;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final T self() {
+ return (T) this;
+ }
+
+ public T goTo() {
+ page.navigate(pageUrl);
+ waitForLoaded();
+ return self();
+ }
+
+ /** Assert a stable landmark so callers know the page has rendered. */
+ protected abstract void waitForLoaded();
+
+ // --- React add/edit dialog (dialog.rsp-dialog) ---
+
+ /** The React add/edit dialog. */
+ protected Locator dialog() {
+ return page.locator("dialog.rsp-dialog");
+ }
+
+ /** Click the dialog's primary button and wait for it to close. */
+ public T submitDialog() {
+ dialog().locator("button.jenkins-button--primary").click();
+ assertThat(dialog()).hasCount(0);
+ return self();
+ }
+
+ // --- Jenkins core confirm dialog (dialog.confirm) ---
+
+ /** Jenkins core's confirm dialog — a plain jenkins-dialog, not the React one. */
+ protected Locator confirmDialog() {
+ return page.locator("dialog.jenkins-dialog:not(.rsp-dialog)");
+ }
+
+ public T confirmDialogContainsText(String text) {
+ assertThat(confirmDialog()).containsText(text);
+ return self();
+ }
+
+ /** Click the confirm dialog's OK button (data-id="ok") and wait for it to close. */
+ public T acceptConfirmDialog() {
+ confirmDialog().locator("[data-id='ok']").click();
+ assertThat(confirmDialog()).hasCount(0);
+ return self();
+ }
+
+ /** Click the confirm dialog's Cancel button (data-id="cancel") and wait for it to close. */
+ public T dismissConfirmDialog() {
+ confirmDialog().locator("[data-id='cancel']").click();
+ assertThat(confirmDialog()).hasCount(0);
+ return self();
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..cdcda8b2
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "strict": true,
+ "noImplicitAny": true,
+ "useUnknownInCatchVariables": false,
+ "types": ["node", "vitest/globals", "@testing-library/jest-dom/vitest"]
+ },
+ "include": ["./src/main/frontend/"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..1736ad05
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,28 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
+
+export default defineConfig({
+ base: "./",
+ plugins: [
+ react(),
+ cssInjectedByJsPlugin({
+ relativeCSSInjection: true,
+ }),
+ ],
+ build: {
+ sourcemap: true,
+ cssCodeSplit: true,
+ rollupOptions: {
+ input: {
+ "manage-roles": "src/main/frontend/manage-roles/index.tsx",
+ "permission-templates":
+ "src/main/frontend/permission-templates/index.tsx",
+ },
+ output: {
+ entryFileNames: "[name]-bundle.js",
+ dir: "src/main/webapp/js/bundles",
+ },
+ },
+ },
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 00000000..f189320f
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ root: "src/main/frontend",
+ globals: true,
+ environment: "jsdom",
+ setupFiles: ["setupTests.ts"],
+ },
+});