Skip to content

Commit 375944c

Browse files
authored
Optimise new credentials dialog (#1016)
1 parent 9049e8c commit 375944c

File tree

5 files changed

+153
-54
lines changed

5 files changed

+153
-54
lines changed

src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ public ModelObject resolveContext(Object context) {
125125
return context instanceof ModelObject mo ? mo : CredentialsDescriptor.findContextInPath(ModelObject.class);
126126
}
127127

128+
@Restricted(NoExternalUse.class)
129+
public boolean hasOneDomain(Map<String, List<CredentialsStoreAction.DomainWrapper>> storeActions) {
130+
// Count the number of domain wrappers across all store actions. If there is only one, return true else false
131+
return storeActions.values().stream().mapToInt(List::size).sum() == 1;
132+
}
133+
128134
/**
129135
* @return modifiable store actions for the context provided.
130136
*/

src/main/resources/com/cloudbees/plugins/credentials/ViewCredentialsAction/index.jelly

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,45 @@
4343

4444
<j:if test="${hasCreatePermission}">
4545
<j:set var="addCredentialsButton">
46-
<l:overflowButton text="${%Add Credentials}"
47-
icon="symbol-add"
48-
clazz="jenkins-button--primary"
49-
tooltip="${null}">
50-
<j:forEach var="store" items="${modifiableStoreActions}">
51-
<dd:header text="${store.key}"/>
52-
<j:forEach var="domain" items="${store.value}">
53-
<j:set var="relativePath" value="${domain.store.getRelativeLinkTo(domain.domain)}" />
54-
<dd:custom>
55-
<button class="jenkins-dropdown__item"
56-
data-type="credentials-add-store-item"
57-
data-url="${relativePath}dialog?relativePath=${relativePath}"
58-
type="button">
59-
<div class="jenkins-dropdown__item__icon">
60-
<l:icon src="${domain.iconClassName}"/>
61-
</div>
62-
<span>${domain.displayName}</span>
63-
<j:if test="${!empty(domain.description)}">
64-
<div class="jenkins-dropdown__item__description">${domain.description}</div>
65-
</j:if>
66-
</button>
67-
</dd:custom>
68-
</j:forEach>
69-
</j:forEach>
70-
</l:overflowButton>
46+
<j:choose>
47+
<j:when test="${selectHelper.hasOneDomain(modifiableStoreActions)}">
48+
<j:set var="domain" value="${modifiableStoreActions.values().iterator().next().get(0)}"/>
49+
<j:set var="relativePath" value="${domain.store.getRelativeLinkTo(domain.domain)}"/>
50+
<button class="jenkins-button jenkins-button--primary"
51+
data-type="credentials-add-store-item"
52+
data-url="${relativePath}dialog?relativePath=${relativePath}">
53+
<l:icon src="symbol-add"/>
54+
${%Add Credentials}
55+
</button>
56+
</j:when>
57+
<j:otherwise>
58+
<l:overflowButton text="${%Add Credentials}"
59+
icon="symbol-add"
60+
clazz="jenkins-button--primary"
61+
tooltip="${null}">
62+
<j:forEach var="store" items="${modifiableStoreActions}">
63+
<dd:header text="${store.key}"/>
64+
<j:forEach var="domain" items="${store.value}">
65+
<j:set var="relativePath" value="${domain.store.getRelativeLinkTo(domain.domain)}" />
66+
<dd:custom>
67+
<button class="jenkins-dropdown__item"
68+
data-type="credentials-add-store-item"
69+
data-url="${relativePath}dialog?relativePath=${relativePath}"
70+
type="button">
71+
<div class="jenkins-dropdown__item__icon">
72+
<l:icon src="${domain.iconClassName}"/>
73+
</div>
74+
<span>${domain.displayName}</span>
75+
<j:if test="${!empty(domain.description)}">
76+
<div class="jenkins-dropdown__item__description">${domain.description}</div>
77+
</j:if>
78+
</button>
79+
</dd:custom>
80+
</j:forEach>
81+
</j:forEach>
82+
</l:overflowButton>
83+
</j:otherwise>
84+
</j:choose>
7185
</j:set>
7286
</j:if>
7387

src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
4040

4141
@WithJenkins
42-
class CredentialsSelectHelperTest {
42+
public class CredentialsSelectHelperTest {
4343

4444
private JenkinsRule j;
4545

@@ -223,7 +223,7 @@ private HtmlForm selectPEMCertificateKeyStore(HtmlPage htmlPage, JenkinsRule.Web
223223
return form;
224224
}
225225

226-
private static boolean selectOption(DomNodeList<DomNode> allOptions, String optionName) {
226+
public static boolean selectOption(DomNodeList<DomNode> allOptions, String optionName) {
227227
return allOptions.stream().anyMatch(domNode -> {
228228
if (domNode instanceof HtmlDivision option) {
229229
if (option.getVisibleText().contains(optionName)) {

src/test/java/com/cloudbees/plugins/credentials/ViewCredentialsActionTest.java

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
package com.cloudbees.plugins.credentials;
22

3+
import static com.cloudbees.plugins.credentials.CredentialsSelectHelperTest.selectOption;
34
import static com.cloudbees.plugins.credentials.XmlMatchers.isSimilarToIgnoringPrivateAttrs;
45
import static org.hamcrest.MatcherAssert.assertThat;
6+
import static org.hamcrest.Matchers.hasSize;
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
58
import static org.junit.jupiter.api.Assertions.assertFalse;
69
import static org.junit.jupiter.api.Assertions.assertNotNull;
710
import static org.junit.jupiter.api.Assertions.assertNull;
811
import static org.junit.jupiter.api.Assertions.assertTrue;
912

13+
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
14+
import com.cloudbees.plugins.credentials.domains.Domain;
15+
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
16+
import hudson.ExtensionList;
17+
import hudson.model.FreeStyleProject;
18+
import hudson.security.ACL;
19+
import java.io.IOException;
1020
import java.util.ArrayList;
1121
import java.util.Collections;
1222
import java.util.List;
1323
import java.util.Random;
14-
1524
import org.htmlunit.WebResponse;
25+
import org.htmlunit.html.DomNode;
26+
import org.htmlunit.html.DomNodeList;
27+
import org.htmlunit.html.HtmlButton;
28+
import org.htmlunit.html.HtmlElementUtil;
29+
import org.htmlunit.html.HtmlForm;
30+
import org.htmlunit.html.HtmlPage;
31+
import org.htmlunit.html.HtmlRadioButtonInput;
1632
import org.junit.jupiter.api.Test;
1733
import org.jvnet.hudson.test.JenkinsRule;
18-
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
19-
20-
import com.cloudbees.plugins.credentials.domains.Domain;
21-
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
22-
23-
import hudson.ExtensionList;
24-
import hudson.model.FreeStyleProject;
25-
2634
import org.jvnet.hudson.test.MockFolder;
35+
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
2736

2837
@WithJenkins
2938
class ViewCredentialsActionTest {
@@ -146,5 +155,92 @@ void isVisibleShouldReturnFalseForRegularJobs(JenkinsRule j) throws Exception {
146155
assertNull(action.getIconFileName(),
147156
"getIconFileName() should return null when action is not visible");
148157
}
158+
159+
@Test
160+
void createUsernamePasswordCredentials(JenkinsRule r) throws Exception {
161+
createUsernamePasswordCredentials(r, false);
162+
}
163+
164+
@Test
165+
void createUsernamePasswordCredentialsWithMultipleDomains(JenkinsRule r) throws Exception {
166+
createTestDomain(r);
167+
createUsernamePasswordCredentials(r, true);
168+
}
169+
170+
private static void createTestDomain(JenkinsRule r) throws IOException {
171+
// Create a test domain here, so there are at least 2 domains to cause the
172+
// Add credentials button to render a domain selector.
173+
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(
174+
SystemCredentialsProvider.ProviderImpl.class);
175+
CredentialsStore systemStore = system.getStore(r.getInstance());
176+
String domainName = "test-domain";
177+
String domainDescription = "test description";
178+
systemStore.addDomain(new Domain(domainName, domainDescription, Collections.emptyList()));
179+
}
180+
181+
private void createUsernamePasswordCredentials(JenkinsRule r, boolean clickGlobalDomain) throws Exception {
182+
String displayName = r.jenkins.getDescriptor(UsernamePasswordCredentialsImpl.class).getDisplayName();
183+
184+
if (clickGlobalDomain) {
185+
// Ensure there is more than one domain so the UI exposes a domain selector.
186+
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class)
187+
.get(SystemCredentialsProvider.ProviderImpl.class);
188+
CredentialsStore systemStore = system.getStore(r.jenkins);
189+
systemStore.addDomain(new Domain("extra-domain", "Extra domain for UI test", Collections.emptyList()),
190+
Collections.emptyList());
191+
}
192+
193+
try (JenkinsRule.WebClient wc = r.createWebClient()) {
194+
HtmlPage htmlPage = wc.goTo("credentials/");
195+
196+
HtmlButton button = htmlPage.querySelector(".jenkins-button--primary");
197+
HtmlElementUtil.click(button);
198+
199+
if (clickGlobalDomain) {
200+
button = htmlPage.querySelector("button[data-type='credentials-add-store-item']");
201+
HtmlElementUtil.click(button);
202+
}
203+
204+
HtmlForm form = htmlPage.getFormByName("dialog");
205+
206+
DomNodeList<DomNode> allOptions = form.querySelectorAll(".jenkins-choice-list__item");
207+
boolean optionFound = selectOption(allOptions, displayName);
208+
assertTrue(optionFound, "The username password option was not found in the credentials type select");
209+
210+
HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-next");
211+
HtmlElementUtil.click(formSubmitButton);
212+
213+
HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials");
214+
215+
if (clickGlobalDomain) {
216+
// Best-effort: if the domain radio list is present, pick the global domain.
217+
// (When only one domain exists, Jenkins often omits the selector entirely.)
218+
DomNodeList<DomNode> radios = newCredentialsForm.querySelectorAll("input[type='radio'][name$='domain']");
219+
for (DomNode n : radios) {
220+
if (n instanceof HtmlRadioButtonInput rbi && rbi.getValueAttribute() != null
221+
&& ("_".equals(rbi.getValueAttribute()) || "global".equalsIgnoreCase(rbi.getValueAttribute()))) {
222+
rbi.setChecked(true);
223+
break;
224+
}
225+
}
226+
}
227+
228+
newCredentialsForm.getInputByName("_.username").setValue("username");
229+
newCredentialsForm.getInputByName("_.password").setValue("password");
230+
231+
List<UsernamePasswordCredentials> credentials = CredentialsProvider.lookupCredentialsInItemGroup(UsernamePasswordCredentials.class, null, ACL.SYSTEM2);
232+
assertThat(credentials, hasSize(0));
233+
234+
button = newCredentialsForm.querySelector("#cr-dialog-submit");
235+
HtmlElementUtil.click(button);
236+
237+
credentials = CredentialsProvider.lookupCredentialsInItemGroup(UsernamePasswordCredentials.class, null, ACL.SYSTEM2);
238+
assertThat(credentials, hasSize(1));
239+
240+
UsernamePasswordCredentials passwordCredentials = credentials.get(0);
241+
String username = passwordCredentials.getUsername();
242+
assertEquals("username", username);
243+
}
244+
}
149245

150246
}

src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import java.util.Base64;
6969
import java.util.List;
7070

71+
import static com.cloudbees.plugins.credentials.CredentialsSelectHelperTest.selectOption;
7172
import static org.hamcrest.MatcherAssert.assertThat;
7273
import static org.hamcrest.Matchers.hasSize;
7374
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -310,24 +311,6 @@ void fullSubmitOfUploadedPEM() throws Exception {
310311
assertEquals(EXPECTED_DISPLAY_NAME_PEM, displayName);
311312
}
312313

313-
private static boolean selectOption(DomNodeList<DomNode> allOptions, String optionDisplayName) {
314-
return allOptions.stream().anyMatch(domNode -> {
315-
if (domNode instanceof HtmlDivision option) {
316-
if (option.getVisibleText().contains(optionDisplayName)) {
317-
try {
318-
HtmlRadioButtonInput item = domNode.querySelector(".jenkins-choice-list__item input");
319-
HtmlElementUtil.click(item);
320-
} catch (IOException e) {
321-
throw new RuntimeException(e);
322-
}
323-
return true;
324-
}
325-
}
326-
327-
return false;
328-
});
329-
}
330-
331314
private String getValidP12_base64() throws Exception {
332315
return Base64.getEncoder().encodeToString(Files.readAllBytes(p12.toPath()));
333316
}

0 commit comments

Comments
 (0)