[FIX JENKINS-39625] Page preloading with pipeline state - Runs (#613)
* Introduce PageStatePreloader extension point * Added BlueoceanUrl to blueocean-commons * Updates to BlueoceanUrl * Added PipelineStatePreloader to dashboard * PageStatePreloader impl for js-extensions data * RESTFetchPreloader * CapabilityAugmenter changes to handle an Array of _class names * Client side mods to handle prefetchdata * Updated deps for core-js * Add a test for PipelineStatePreloader * Rename BlueoceanUrl to BlueOceanUrl * Rename BlueOceanUrl to BlueUrlTokenizer * Added a comment to BlueUrlTokenizer * Fix Javadoc errors * core-js version 0.0.35 * Updating to @jenkins-cd/blueocean-core-js@0.0.35 on dependants
This commit is contained in:
parent
dc4e9006dc
commit
e25ae38a26
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import hudson.model.Run;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* General purpose Blue Ocean UI URL parser.
|
||||
* <p>
|
||||
* This class performs a "best effort" attempt to parse a URL as a Blue Ocean
|
||||
* URL, extracting what it thinks are the relevant "parts" and making available via the
|
||||
* {@link #getPart(UrlPart)} and {@link #hasPart(UrlPart)} functions.
|
||||
* <p>
|
||||
* See TBD comment on {@link UrlPart}.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@Restricted(NoExternalUse.class) // Internal use only for now because there's a fair chance we'll change how it works. See TBD comment on UrlPart.
|
||||
public class BlueUrlTokenizer {
|
||||
|
||||
private static final Set<String> PIPELINE_TABS =
|
||||
new LinkedHashSet<>(Arrays.asList("activity", "branches", "pr"));
|
||||
|
||||
private static final Set<String> PIPELINE_RUN_DETAIL_TABS =
|
||||
new LinkedHashSet<>(Arrays.asList("pipeline", "changes", "tests", "artifacts"));
|
||||
|
||||
private Map<UrlPart, String> urlParts = new LinkedHashMap<>();
|
||||
private UrlPart lastPart;
|
||||
|
||||
/**
|
||||
* Enum of URL "parts".
|
||||
* <p>
|
||||
* Use {@link #getPart(UrlPart)} to get a specific URL "part",
|
||||
* or call {@link #hasPart(UrlPart)} to check for it's existence.
|
||||
* <p>
|
||||
* *** TBD: decide whether to stick with this model, or to switch to more of a straight getters/setters style on the {@link BlueUrlTokenizer} instance.
|
||||
* Reason for trying this approach ("parts" enum) is that I (TF) think the straight properties style with getters/setters
|
||||
* would get messy as we add support for parsing more URL paths/parts i.e. a getters/setters API explosion.
|
||||
* That said ... not sure I love this approach either, hence marked BlueoceanUrl as @Restricted(NoExternalUse.class). Let's suck it
|
||||
* and see for a bit and change if it sucks :)
|
||||
*/
|
||||
public enum UrlPart {
|
||||
/**
|
||||
* Main blue ocean pipelines dashboard.
|
||||
* i.e. /blue/pipelines/
|
||||
*/
|
||||
DASHBOARD_PIPELINES,
|
||||
/**
|
||||
* A URL pointing at a page associated with an "organization" resource.
|
||||
* e.g. /blue/organizations/jenkins/...
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the organization name.
|
||||
*/
|
||||
ORGANIZATION,
|
||||
/**
|
||||
* A URL pointing at a pipeline.
|
||||
* e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/...
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the pipeline name. Note that the URL
|
||||
* may have additional parts (e.g. {@link UrlPart#PIPELINE_TAB} or {@link UrlPart#PIPELINE_RUN_DETAIL}).
|
||||
*/
|
||||
PIPELINE,
|
||||
/**
|
||||
* A URL pointing at a pipeline tab.
|
||||
* e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the tab name.
|
||||
*/
|
||||
PIPELINE_TAB,
|
||||
/**
|
||||
* A URL pointing at a pipeline Run Details.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/...
|
||||
* <p>
|
||||
* See {@link #BRANCH} for sub-component of this URL.
|
||||
*/
|
||||
PIPELINE_RUN_DETAIL,
|
||||
/**
|
||||
* A URL pointing at a pipeline Run Details for a specific branch.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/...
|
||||
* <p>
|
||||
* See {@link #PIPELINE_RUN_DETAIL_ID} for sub-component of this URL.
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the branch name.
|
||||
*/
|
||||
BRANCH,
|
||||
/**
|
||||
* A URL pointing at a pipeline Run Details for a specific run of a specific branch.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/...
|
||||
* <p>
|
||||
* See {@link #PIPELINE_RUN_DETAIL_ID} for sub-component of this URL.
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the {@link Run} ID.
|
||||
*/
|
||||
PIPELINE_RUN_DETAIL_ID,
|
||||
/**
|
||||
* A URL pointing at one of the tabs on a pipeline Run Details for a specific run of a specific branch.
|
||||
* e.g. // e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/artifacts
|
||||
* <p>
|
||||
* Call Use {@link #getPart(UrlPart)} to get the tab name.
|
||||
*/
|
||||
PIPELINE_RUN_DETAIL_TAB,
|
||||
}
|
||||
|
||||
private BlueUrlTokenizer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the {@link Stapler#getCurrentRequest() current Stapler request} and return a {@link BlueUrlTokenizer} instance
|
||||
* iff the URL is a Blue Ocean UI URL.
|
||||
*
|
||||
* @return A {@link BlueUrlTokenizer} instance iff the URL is a Blue Ocean UI URL, otherwise {@code null}.
|
||||
* @throws IllegalStateException Called outside the scope of an active {@link StaplerRequest}.
|
||||
*/
|
||||
public static @CheckForNull
|
||||
BlueUrlTokenizer parseCurrentRequest() throws IllegalStateException {
|
||||
StaplerRequest currentRequest = Stapler.getCurrentRequest();
|
||||
|
||||
if (currentRequest == null) {
|
||||
throw new IllegalStateException("Illegal call to BlueoceanUrl.parseCurrentRequest outside the scope of an active StaplerRequest.");
|
||||
}
|
||||
|
||||
String path = currentRequest.getOriginalRequestURI();
|
||||
String contextPath = currentRequest.getContextPath();
|
||||
|
||||
path = path.substring(contextPath.length());
|
||||
|
||||
return parse(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the supplied URL string and return a {@link BlueUrlTokenizer} instance
|
||||
* iff the URL is a Blue Ocean UI URL.
|
||||
*
|
||||
* @param url The URL to be parsed. The URL must not be decoded in any way, so as to ensure
|
||||
* that no URL component data is lost.
|
||||
* @return A {@link BlueUrlTokenizer} instance iff the URL is a Blue Ocean UI URL, otherwise {@code null}.
|
||||
*/
|
||||
public static @CheckForNull
|
||||
BlueUrlTokenizer parse(@Nonnull String url) {
|
||||
Iterator<String> urlTokens = extractTokens(url);
|
||||
|
||||
//
|
||||
// Yes, the following code is quite ugly, but it's easy enough to understand atm.
|
||||
// Unless this gets a lot more detailed, please don't get super clever ideas about using
|
||||
// some fancy-pants abstractions/patterns/3rd-party-libs for parsing the URL that, while
|
||||
// might make the code look neater structurally, also makes the code logic a lot harder
|
||||
// to follow (without using a debugger).
|
||||
//
|
||||
if (urlTokens.hasNext()) {
|
||||
if (urlTokens.next().equalsIgnoreCase("blue")) {
|
||||
BlueUrlTokenizer blueUrlTokenizer = new BlueUrlTokenizer();
|
||||
|
||||
if (urlTokens.hasNext()) {
|
||||
String next = urlTokens.next();
|
||||
|
||||
if (next.equalsIgnoreCase("pipelines")) {
|
||||
// i.e. /blue/pipelines/
|
||||
blueUrlTokenizer.addPart(UrlPart.DASHBOARD_PIPELINES, next);
|
||||
} else if (next.equalsIgnoreCase("organizations")) {
|
||||
// i.e. /blue/organizations/...
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/...
|
||||
blueUrlTokenizer.addPart(UrlPart.ORGANIZATION, urlTokens.next());
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/...
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE, urlDecode(urlTokens.next()));
|
||||
if (urlTokens.hasNext()) {
|
||||
next = urlTokens.next();
|
||||
if (next.equalsIgnoreCase("detail")) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/...
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_RUN_DETAIL, next);
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/...
|
||||
blueUrlTokenizer.addPart(UrlPart.BRANCH, urlDecode(urlTokens.next()));
|
||||
if (urlTokens.hasNext()) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/...
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_RUN_DETAIL_ID, urlDecode(urlTokens.next()));
|
||||
if (urlTokens.hasNext()) {
|
||||
next = urlTokens.next();
|
||||
if (PIPELINE_RUN_DETAIL_TABS.contains(next.toLowerCase())) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/pipeline
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_RUN_DETAIL_TAB, next.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (PIPELINE_TABS.contains(next.toLowerCase())) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity/
|
||||
blueUrlTokenizer.addPart(UrlPart.PIPELINE_TAB, next.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blueUrlTokenizer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addPart(@Nonnull UrlPart urlPart, @Nonnull String value) {
|
||||
urlParts.put(urlPart, value);
|
||||
this.lastPart = urlPart;
|
||||
}
|
||||
|
||||
public boolean hasPart(@Nonnull UrlPart urlPart) {
|
||||
return urlParts.containsKey(urlPart);
|
||||
}
|
||||
|
||||
public @CheckForNull String getPart(@Nonnull UrlPart urlPart) {
|
||||
return urlParts.get(urlPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last {@link UrlPart} for the URL.
|
||||
* @return The last {@link UrlPart} for the URL.
|
||||
*/
|
||||
public @CheckForNull UrlPart getLastPart() {
|
||||
return this.lastPart;
|
||||
}
|
||||
|
||||
public boolean lastPartIs(@Nonnull UrlPart urlPart) {
|
||||
return this.lastPart == urlPart;
|
||||
}
|
||||
|
||||
public boolean lastPartIs(@Nonnull UrlPart urlPart, @Nonnull String value) {
|
||||
if (this.lastPart == urlPart) {
|
||||
return getPart(this.lastPart).equals(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String urlDecode(String string) {
|
||||
try {
|
||||
return URLDecoder.decode(string, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException("Unexpected UnsupportedEncodingException for UTF-8.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Iterator<String> extractTokens(String url) {
|
||||
String[] uncleanedTokens = url.split("/");
|
||||
List<String> cleanedTokens = new ArrayList<>();
|
||||
|
||||
for (String uncleanedToken : uncleanedTokens) {
|
||||
if (uncleanedToken.length() != 0) {
|
||||
cleanedTokens.add(uncleanedToken);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedTokens.iterator();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import org.apache.tools.ant.ExtensionPoint;
|
||||
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Page state "preloader" extension point.
|
||||
* <p>
|
||||
* Allows the loading page's JavaScript blueocean global scope to
|
||||
* be pre-populated with data that we know the page is going to need, thereby
|
||||
* providing a mechanism for eliminating the request overhead for that data.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public abstract class PageStatePreloader extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* Get the JavaScript object graph path at shiwh the state is to be stored.
|
||||
* @return The JavaScript object graph path at shiwh the state is to be stored.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract String getStatePropertyPath();
|
||||
|
||||
/**
|
||||
* Get the state JSON to be set in the page's JavaScript blueocean global scope.
|
||||
* @return The state JSON to be set in the page's JavaScript blueocean global
|
||||
* scope, or {@code null} if no data is to be set of this page.
|
||||
*/
|
||||
@CheckForNull
|
||||
public abstract String getStateJson();
|
||||
|
||||
public static ExtensionList<PageStatePreloader> all() {
|
||||
return ExtensionList.lookup(PageStatePreloader.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* REST prefetch data preloader.
|
||||
* <p>
|
||||
* Pre-populates the page with REST data, allowing the client side {@code Fetch}
|
||||
* module (see {@code Fetch} module in the {@code @jenkins-cd/blueocean-core-js NPM packages})
|
||||
* to avoid the REST API call overhead.
|
||||
* <p>
|
||||
* Create implementations of this class (and annotate with {@code @Extension}) for data that
|
||||
* we know is going to be needed by the page.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public abstract class RESTFetchPreloader extends PageStatePreloader {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public final String getStatePropertyPath() {
|
||||
return "prefetchdata." + getClass().getSimpleName();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public final String getStateJson() {
|
||||
BlueUrlTokenizer blueUrl = BlueUrlTokenizer.parseCurrentRequest();
|
||||
|
||||
if (blueUrl == null) {
|
||||
// Not a Blue Ocean page, so nothing to be added.
|
||||
return null;
|
||||
}
|
||||
|
||||
FetchData fetchData = getFetchData(blueUrl);
|
||||
if (fetchData != null) {
|
||||
return fetchData.toJSON();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract FetchData getFetchData(@Nonnull BlueUrlTokenizer blueUrl);
|
||||
|
||||
public static final class FetchData {
|
||||
|
||||
private String restUrl;
|
||||
private String data;
|
||||
|
||||
public FetchData(@Nonnull String restUrl, @Nonnull String data) {
|
||||
this.restUrl = restUrl;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getRestUrl() {
|
||||
return restUrl;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String toJSON() {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("restUrl", restUrl);
|
||||
json.put("data", data);
|
||||
return json.toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.commons;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public class BlueUrlTokenizerTest {
|
||||
|
||||
@Test
|
||||
public void test_MalformedURLException() {
|
||||
Assert.assertNull(BlueUrlTokenizer.parse("/a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
BlueUrlTokenizer blueUrl;
|
||||
|
||||
blueUrl = BlueUrlTokenizer.parse("/blue/pipelines/");
|
||||
Assert.assertEquals("pipelines", blueUrl.getPart(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
Assert.assertEquals(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES, blueUrl.getLastPart());
|
||||
Assert.assertTrue(blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
|
||||
blueUrl = BlueUrlTokenizer.parse("/blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity/");
|
||||
Assert.assertFalse(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
Assert.assertTrue(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("jenkins", blueUrl.getPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("f1/f3 with spaces/f3 pipeline", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE));
|
||||
Assert.assertEquals("activity", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE_TAB));
|
||||
Assert.assertEquals(BlueUrlTokenizer.UrlPart.PIPELINE_TAB, blueUrl.getLastPart());
|
||||
Assert.assertTrue(blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.PIPELINE_TAB, "activity"));
|
||||
|
||||
blueUrl = BlueUrlTokenizer.parse("/blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/detail/magic-branch-X/55/pipeline");
|
||||
Assert.assertFalse(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.DASHBOARD_PIPELINES));
|
||||
Assert.assertTrue(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("jenkins", blueUrl.getPart(BlueUrlTokenizer.UrlPart.ORGANIZATION));
|
||||
Assert.assertEquals("f1/f3 with spaces/f3 pipeline", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE));
|
||||
Assert.assertFalse(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.PIPELINE_TAB));
|
||||
Assert.assertTrue(blueUrl.hasPart(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL));
|
||||
Assert.assertEquals("magic-branch-X", blueUrl.getPart(BlueUrlTokenizer.UrlPart.BRANCH));
|
||||
Assert.assertEquals("55", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_ID));
|
||||
Assert.assertEquals("pipeline", blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_TAB));
|
||||
Assert.assertEquals(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_TAB, blueUrl.getLastPart());
|
||||
Assert.assertTrue(blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.PIPELINE_RUN_DETAIL_TAB, "pipeline"));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.User;
|
||||
import hudson.security.AuthorizationStrategy;
|
||||
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
|
||||
import hudson.security.SecurityRealm;
|
||||
import io.jenkins.blueocean.BluePageDecorator;
|
||||
import io.jenkins.blueocean.commons.BlueOceanConfigProperties;
|
||||
import io.jenkins.blueocean.commons.stapler.ModelObjectSerializer;
|
||||
import io.jenkins.blueocean.service.embedded.rest.UserImpl;
|
||||
import jenkins.model.Jenkins;
|
||||
import net.sf.json.JSONObject;
|
||||
import net.sf.json.util.JSONBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension(ordinal = 10)
|
||||
public class BlueOceanConfig extends BluePageDecorator {
|
||||
|
||||
public boolean isRollBarEnabled(){
|
||||
return BlueOceanConfigProperties.ROLLBAR_ENABLED;
|
||||
}
|
||||
|
||||
public String getBlueOceanUser() throws IOException {
|
||||
try (StringWriter writer = new StringWriter()) {
|
||||
User currentUser = User.current();
|
||||
JSONObject currentUserJson;
|
||||
|
||||
if (currentUser != null) {
|
||||
currentUserJson = JSONObject.fromObject(ModelObjectSerializer.toJson(new UserImpl(currentUser)));
|
||||
} else {
|
||||
currentUserJson = new JSONObject();
|
||||
currentUserJson.put("id", "anonymous");
|
||||
}
|
||||
|
||||
new JSONBuilder(writer)
|
||||
.object()
|
||||
.key("user").value(currentUserJson)
|
||||
.endObject();
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public String getBlueOceanConfig() throws IOException {
|
||||
try (StringWriter writer = new StringWriter()) {
|
||||
Jenkins jenkins = Jenkins.getInstance();
|
||||
String version = Jenkins.getVersion() != null ? Jenkins.getVersion().toString() : Jenkins.VERSION;
|
||||
|
||||
AuthorizationStrategy authorizationStrategy = jenkins.getAuthorizationStrategy();
|
||||
boolean allowAnonymousRead = true;
|
||||
if(authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy){
|
||||
allowAnonymousRead = ((FullControlOnceLoggedInAuthorizationStrategy) authorizationStrategy).isAllowAnonymousRead();
|
||||
}
|
||||
|
||||
new JSONBuilder(writer)
|
||||
.object()
|
||||
.key("version").value(getBlueOceanPluginVersion())
|
||||
.key("jenkinsConfig")
|
||||
.object()
|
||||
.key("version").value(version)
|
||||
.key("security")
|
||||
.object()
|
||||
.key("enabled").value(jenkins.isUseSecurity())
|
||||
.key("loginUrl").value(jenkins.getSecurityRealm() == SecurityRealm.NO_AUTHENTICATION ? null : jenkins.getSecurityRealm().getLoginUrl())
|
||||
.key("authorizationStrategy").object()
|
||||
.key("allowAnonymousRead").value(allowAnonymousRead)
|
||||
.endObject()
|
||||
.key("enableJWT").value(BlueOceanConfigProperties.BLUEOCEAN_FEATURE_JWT_AUTHENTICATION)
|
||||
.endObject()
|
||||
.endObject()
|
||||
.endObject();
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public String getJsExtensions() {
|
||||
return JenkinsJSExtensions.getExtensionsData().toString();
|
||||
}
|
||||
|
||||
/** gives Blueocean plugin version. blueocean-web being core module is looked at to determine the version */
|
||||
private String getBlueOceanPluginVersion(){
|
||||
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.security.AuthorizationStrategy;
|
||||
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
|
||||
import hudson.security.SecurityRealm;
|
||||
import io.jenkins.blueocean.commons.BlueOceanConfigProperties;
|
||||
import io.jenkins.blueocean.commons.PageStatePreloader;
|
||||
import jenkins.model.Jenkins;
|
||||
import net.sf.json.util.JSONBuilder;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension
|
||||
public class BlueOceanConfigStatePreloader extends PageStatePreloader {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(BlueOceanConfigStatePreloader.class.getName());
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStatePropertyPath() {
|
||||
return "config";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStateJson() {
|
||||
StringWriter writer = new StringWriter();
|
||||
Jenkins jenkins = Jenkins.getInstance();
|
||||
String version = Jenkins.getVersion() != null ? Jenkins.getVersion().toString() : Jenkins.VERSION;
|
||||
|
||||
AuthorizationStrategy authorizationStrategy = jenkins.getAuthorizationStrategy();
|
||||
boolean allowAnonymousRead = true;
|
||||
if(authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy){
|
||||
allowAnonymousRead = ((FullControlOnceLoggedInAuthorizationStrategy) authorizationStrategy).isAllowAnonymousRead();
|
||||
}
|
||||
|
||||
new JSONBuilder(writer)
|
||||
.object()
|
||||
.key("version").value(getBlueOceanPluginVersion())
|
||||
.key("jenkinsConfig")
|
||||
.object()
|
||||
.key("version").value(version)
|
||||
.key("security")
|
||||
.object()
|
||||
.key("enabled").value(jenkins.isUseSecurity())
|
||||
.key("loginUrl").value(jenkins.getSecurityRealm() == SecurityRealm.NO_AUTHENTICATION ? null : jenkins.getSecurityRealm().getLoginUrl())
|
||||
.key("authorizationStrategy").object()
|
||||
.key("allowAnonymousRead").value(allowAnonymousRead)
|
||||
.endObject()
|
||||
.key("enableJWT").value(BlueOceanConfigProperties.BLUEOCEAN_FEATURE_JWT_AUTHENTICATION)
|
||||
.endObject()
|
||||
.endObject()
|
||||
.endObject();
|
||||
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
/** gives Blueocean plugin version. blueocean-web being core module is looked at to determine the version */
|
||||
private String getBlueOceanPluginVersion(){
|
||||
return Jenkins.getInstance().getPlugin("blueocean-web").getWrapper().getVersion();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import io.jenkins.blueocean.commons.PageStatePreloader;
|
||||
|
||||
/**
|
||||
* {@link PageStatePreloader} for js-extensions data.
|
||||
*/
|
||||
@Extension
|
||||
public class JenkinsJSExtensionsStatePreloader extends PageStatePreloader {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStatePropertyPath() {
|
||||
return "jsExtensions";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStateJson() {
|
||||
return JenkinsJSExtensions.getExtensionsData().toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package io.jenkins.blueocean.config;
|
||||
|
||||
import hudson.Extension;
|
||||
import io.jenkins.blueocean.BluePageDecorator;
|
||||
import io.jenkins.blueocean.commons.BlueOceanConfigProperties;
|
||||
|
||||
/**
|
||||
* @author Vivek Pandey
|
||||
*/
|
||||
@Extension(ordinal = 10)
|
||||
public class RollbarDecorator extends BluePageDecorator {
|
||||
|
||||
public boolean isRollBarEnabled(){
|
||||
return BlueOceanConfigProperties.ROLLBAR_ENABLED;
|
||||
}
|
||||
}
|
|
@ -1,13 +1,4 @@
|
|||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
<script>
|
||||
(function () {
|
||||
window.$$blueocean = {};
|
||||
window.$$blueocean.user = ${it.blueOceanUser}.user;
|
||||
window.$$blueocean.config = ${it.blueOceanConfig};
|
||||
window.$$blueocean.jsExtensions = ${it.jsExtensions};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<j:if test="${it.rollBarEnabled}">
|
||||
<!--
|
||||
Running the plugin build (or just "gulp" from the command line) will
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@jenkins-cd/blueocean-core-js",
|
||||
"version": "0.0.35-unpublished",
|
||||
"version": "0.0.35",
|
||||
"description": "Shared JavaScript libraries for use with Jenkins Blue Ocean",
|
||||
"main": "dist/js/index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -3,7 +3,20 @@
|
|||
*/
|
||||
|
||||
const addClass = (clazz, classMap) => {
|
||||
const className = clazz._class;
|
||||
let className;
|
||||
|
||||
if (Array.isArray(clazz._class)) {
|
||||
// If it's an array of class names, just take the first.
|
||||
// TODO: Hmmm ... not sure if this is the right thing to do when we have an array of class names.
|
||||
// Not sure what the array is about tbh. What are the relationships?
|
||||
if (clazz._class.length > 0) {
|
||||
className = clazz._class[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
className = clazz._class;
|
||||
}
|
||||
|
||||
if (!classMap[className]) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
|
|
@ -5,6 +5,9 @@ import utils from './utils';
|
|||
import config from './config';
|
||||
import dedupe from './utils/dedupe-calls';
|
||||
import urlconfig from './urlconfig';
|
||||
import { prefetchdata } from './scopes';
|
||||
|
||||
const Promise = es6Promise.Promise;
|
||||
|
||||
import { capabilityAugmenter } from './capability/index';
|
||||
let refreshToken = null;
|
||||
|
@ -124,10 +127,13 @@ export const FetchFunctions = {
|
|||
*/
|
||||
rawFetchJSON(url, { onSuccess, onError, fetchOptions, disableDedupe } = {}) {
|
||||
const request = () => {
|
||||
const future = isoFetch(url, FetchFunctions.sameOriginFetchOption(fetchOptions))
|
||||
.then(FetchFunctions.checkRefreshHeader)
|
||||
.then(FetchFunctions.checkStatus)
|
||||
.then(FetchFunctions.parseJSON);
|
||||
let future = getPrefetchedDataFuture(url); // eslint-disable-line no-use-before-define
|
||||
if (!future) {
|
||||
future = isoFetch(url, FetchFunctions.sameOriginFetchOption(fetchOptions))
|
||||
.then(FetchFunctions.checkRefreshHeader)
|
||||
.then(FetchFunctions.checkStatus)
|
||||
.then(FetchFunctions.parseJSON);
|
||||
}
|
||||
if (onSuccess) {
|
||||
return future.then(onSuccess).catch(FetchFunctions.onError(onError));
|
||||
}
|
||||
|
@ -155,10 +161,12 @@ export const FetchFunctions = {
|
|||
*/
|
||||
rawFetch(url, { onSuccess, onError, fetchOptions, disableDedupe } = {}) {
|
||||
const request = () => {
|
||||
const future = isoFetch(url, FetchFunctions.sameOriginFetchOption(fetchOptions))
|
||||
.then(FetchFunctions.checkRefreshHeader)
|
||||
.then(FetchFunctions.checkStatus);
|
||||
|
||||
let future = getPrefetchedDataFuture(url); // eslint-disable-line no-use-before-define
|
||||
if (!future) {
|
||||
future = isoFetch(url, FetchFunctions.sameOriginFetchOption(fetchOptions))
|
||||
.then(FetchFunctions.checkRefreshHeader)
|
||||
.then(FetchFunctions.checkStatus);
|
||||
}
|
||||
if (onSuccess) {
|
||||
return future.then(onSuccess).catch(FetchFunctions.onError(onError));
|
||||
}
|
||||
|
@ -224,15 +232,15 @@ export const Fetch = {
|
|||
*/
|
||||
fetch(url, { onSuccess, onError, fetchOptions } = {}) {
|
||||
let fixedUrl = url;
|
||||
|
||||
|
||||
|
||||
|
||||
if (urlconfig.getJenkinsRootURL() !== '' && !url.startsWith(urlconfig.getJenkinsRootURL())) {
|
||||
fixedUrl = `${urlconfig.getJenkinsRootURL()}${url}`;
|
||||
}
|
||||
if (!config.isJWTEnabled()) {
|
||||
return FetchFunctions.rawFetch(fixedUrl, { onSuccess, onError, fetchOptions });
|
||||
}
|
||||
|
||||
|
||||
return jwt.getToken()
|
||||
.then(token => FetchFunctions.rawFetch(fixedUrl, {
|
||||
onSuccess,
|
||||
|
@ -242,3 +250,42 @@ export const Fetch = {
|
|||
},
|
||||
};
|
||||
|
||||
function trimRestUrl(url) {
|
||||
const REST_PREFIX = 'blue/rest/organizations';
|
||||
const prefixOffset = url.indexOf(REST_PREFIX);
|
||||
|
||||
if (prefixOffset !== -1) {
|
||||
return url.substring(prefixOffset);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPrefetchedDataFuture(url) {
|
||||
const trimmedUrl = trimRestUrl(url);
|
||||
|
||||
for (const prop in prefetchdata) {
|
||||
if (prefetchdata.hasOwnProperty(prop)) {
|
||||
const preFetchEntry = prefetchdata[prop];
|
||||
if (preFetchEntry.restUrl && preFetchEntry.data) {
|
||||
// If the trimmed/normalized rest URL matches the url arg supplied
|
||||
// to the function, construct a pre-resolved future object containing
|
||||
// the prefetched data as the value.
|
||||
if (trimRestUrl(preFetchEntry.restUrl) === trimmedUrl) {
|
||||
try {
|
||||
return Promise.resolve(JSON.parse(preFetchEntry.data));
|
||||
} finally {
|
||||
// Delete the preFetchEntry i.e. we only use these entries once. So, this
|
||||
// works only for the first request for the data at that URL. Subsequent
|
||||
// calls on that REST endpoint will result in a proper fetch. A local
|
||||
// store needs to be used (redux/mobx etc) if you want to avoid multiple calls
|
||||
// for the same data. This is not a caching layer/mechanism !!!
|
||||
delete prefetchdata[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -30,3 +30,5 @@ export const root = (typeof self === 'object' && self.self === self && self) ||
|
|||
// and blueocean-config/src/main/resources/io/jenkins/blueocean/config/BlueOceanConfig/header.jelly
|
||||
//
|
||||
export const blueocean = (root.$blueocean || {});
|
||||
|
||||
export const prefetchdata = (blueocean.prefetchdata || {});
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@jenkins-cd/blueocean-core-js": {
|
||||
"version": "0.0.34",
|
||||
"from": "@jenkins-cd/blueocean-core-js@0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@jenkins-cd/blueocean-core-js/-/blueocean-core-js-0.0.34.tgz",
|
||||
"version": "0.0.35",
|
||||
"from": "@jenkins-cd/blueocean-core-js@0.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@jenkins-cd/blueocean-core-js/-/blueocean-core-js-0.0.35.tgz",
|
||||
"dependencies": {
|
||||
"@jenkins-cd/sse-gateway": {
|
||||
"version": "0.0.10",
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"skin-deep": "0.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/blueocean-core-js": "0.0.34",
|
||||
"@jenkins-cd/blueocean-core-js": "0.0.35",
|
||||
"@jenkins-cd/design-language": "0.0.91",
|
||||
"@jenkins-cd/js-extensions": "0.0.32",
|
||||
"@jenkins-cd/js-modules": "0.0.8",
|
||||
|
|
|
@ -18,6 +18,30 @@
|
|||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-events</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test deps -->
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-rest-impl</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mashape.unirest</groupId>
|
||||
<artifactId>unirest-java</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!--
|
||||
jsoup HTML parser library @ http://jsoup.org/
|
||||
Using this to get the raw/unprocessed HTML from Jenkins
|
||||
-->
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.10.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.preload;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.Item;
|
||||
import io.jenkins.blueocean.commons.BlueUrlTokenizer;
|
||||
import io.jenkins.blueocean.commons.RESTFetchPreloader;
|
||||
import io.jenkins.blueocean.commons.stapler.ModelObjectSerializer;
|
||||
import io.jenkins.blueocean.rest.model.BluePipeline;
|
||||
import io.jenkins.blueocean.rest.model.BlueRun;
|
||||
import io.jenkins.blueocean.rest.model.BlueRunContainer;
|
||||
import io.jenkins.blueocean.service.embedded.rest.BluePipelineFactory;
|
||||
import jenkins.model.Jenkins;
|
||||
import net.sf.json.JSONArray;
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Preload pipeline runs onto the page if the requested page is a pipeline runs page.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@Extension
|
||||
public class PipelineStatePreloader extends RESTFetchPreloader {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(PipelineStatePreloader.class.getName());
|
||||
|
||||
private static final int DEFAULT_LIMIT = 26;
|
||||
|
||||
@Override
|
||||
protected FetchData getFetchData(@Nonnull BlueUrlTokenizer blueUrl) {
|
||||
BluePipeline pipeline = getPipeline(blueUrl);
|
||||
|
||||
if (pipeline != null) {
|
||||
// It's a pipeline page. Let's prefetch the pipeline runs and add them to the page,
|
||||
// saving the frontend the overhead of requesting them.
|
||||
|
||||
BlueRunContainer runsContainer = pipeline.getRuns();
|
||||
Iterator<BlueRun> runsIterator = runsContainer.iterator(0, DEFAULT_LIMIT);
|
||||
JSONArray runs = new JSONArray();
|
||||
|
||||
while(runsIterator.hasNext()) {
|
||||
BlueRun blueRun = runsIterator.next();
|
||||
try {
|
||||
runs.add(JSONObject.fromObject(ModelObjectSerializer.toJson(blueRun)));
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.FINE, String.format("Unable to preload runs for Job '%s'. Run serialization error.", pipeline.getFullName()), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new FetchData(
|
||||
pipeline.getActivities().getLink().getHref() + "?start=0&limit=" + DEFAULT_LIMIT,
|
||||
runs.toString());
|
||||
}
|
||||
|
||||
// Don't preload any data on the page.
|
||||
return null;
|
||||
}
|
||||
|
||||
private BluePipeline getPipeline(BlueUrlTokenizer blueUrl) {
|
||||
if (addPipelineRuns(blueUrl)) {
|
||||
Jenkins jenkins = Jenkins.getInstance();
|
||||
String pipelineFullName = blueUrl.getPart(BlueUrlTokenizer.UrlPart.PIPELINE);
|
||||
|
||||
try {
|
||||
Item pipelineJob = jenkins.getItemByFullName(pipelineFullName);
|
||||
return (BluePipeline) BluePipelineFactory.resolve(pipelineJob);
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.FINE, String.format("Unable to find Job named '%s'.", pipelineFullName), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean addPipelineRuns(@Nonnull BlueUrlTokenizer blueUrl) {
|
||||
if (blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.PIPELINE)) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/
|
||||
return true;
|
||||
} else if (blueUrl.lastPartIs(BlueUrlTokenizer.UrlPart.PIPELINE_TAB, "activity")) {
|
||||
// e.g. /blue/organizations/jenkins/f1%2Ff3%20with%20spaces%2Ff3%20pipeline/activity/
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.preload;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.User;
|
||||
import io.jenkins.blueocean.commons.PageStatePreloader;
|
||||
import io.jenkins.blueocean.commons.stapler.ModelObjectSerializer;
|
||||
import io.jenkins.blueocean.service.embedded.rest.UserImpl;
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Preload the user object for the active user.
|
||||
*
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@Extension
|
||||
public class UserStatePreloader extends PageStatePreloader {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(UserStatePreloader.class.getName());
|
||||
|
||||
private static final String ANONYMOUS;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStatePropertyPath() {
|
||||
return "user";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getStateJson() {
|
||||
try {
|
||||
User currentUser = User.current();
|
||||
if (currentUser != null) {
|
||||
return ModelObjectSerializer.toJson(new UserImpl(currentUser));
|
||||
} else {
|
||||
return ANONYMOUS;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "Unexpected error serializing active User object and adding to page preload state.");
|
||||
return ANONYMOUS;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
JSONObject anonUserJson = new JSONObject();
|
||||
anonUserJson.put("id", "anonymous");
|
||||
ANONYMOUS = anonUserJson.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean.preload;
|
||||
|
||||
import hudson.model.FreeStyleBuild;
|
||||
import hudson.model.FreeStyleProject;
|
||||
import io.jenkins.blueocean.BlueOceanWebURLBuilder;
|
||||
import io.jenkins.blueocean.service.embedded.BaseTest;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
public class PipelineStatePreloaderTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
public void test() throws IOException, ExecutionException, InterruptedException, SAXException {
|
||||
// Create a project and run a build on it.
|
||||
FreeStyleProject freestyleProject = j.createProject(FreeStyleProject.class, "freestyle");
|
||||
FreeStyleBuild run = freestyleProject.scheduleBuild2(0).get();
|
||||
j.waitForCompletion(run);
|
||||
|
||||
// Lets request the activity page for that project. The page should
|
||||
// contain some prefetched javascript for the runs on the page
|
||||
String projectBlueUrl = BlueOceanWebURLBuilder.toBlueOceanURL(freestyleProject);
|
||||
Document doc = Jsoup.connect(projectBlueUrl + "/activity/").get();
|
||||
String script = doc.select("head script").toString();
|
||||
|
||||
Assert.assertTrue(script.contains(String.format("setState('prefetchdata.%s',", PipelineStatePreloader.class.getSimpleName())));
|
||||
Assert.assertTrue(script.contains("\"restUrl\":\"/blue/rest/organizations/jenkins/pipelines/freestyle/activities/?start=0&limit=26\""));
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@
|
|||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@jenkins-cd/blueocean-core-js": {
|
||||
"version": "0.0.34",
|
||||
"from": "@jenkins-cd/blueocean-core-js@0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@jenkins-cd/blueocean-core-js/-/blueocean-core-js-0.0.34.tgz",
|
||||
"version": "0.0.35",
|
||||
"from": "@jenkins-cd/blueocean-core-js@0.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@jenkins-cd/blueocean-core-js/-/blueocean-core-js-0.0.35.tgz",
|
||||
"dependencies": {
|
||||
"react-router": {
|
||||
"version": "3.0.0",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"react-addons-test-utils": "15.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/blueocean-core-js": "0.0.34",
|
||||
"@jenkins-cd/blueocean-core-js": "0.0.35",
|
||||
"@jenkins-cd/design-language": "0.0.91",
|
||||
"@jenkins-cd/js-extensions": "0.0.32",
|
||||
"@jenkins-cd/js-modules": "0.0.8",
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@jenkins-cd/blueocean-core-js": {
|
||||
"version": "0.0.34",
|
||||
"from": "@jenkins-cd/blueocean-core-js@0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@jenkins-cd/blueocean-core-js/-/blueocean-core-js-0.0.34.tgz",
|
||||
"version": "0.0.35",
|
||||
"from": "@jenkins-cd/blueocean-core-js@0.0.35",
|
||||
"resolved": "https://registry.npmjs.org/@jenkins-cd/blueocean-core-js/-/blueocean-core-js-0.0.35.tgz",
|
||||
"dependencies": {
|
||||
"history": {
|
||||
"version": "3.2.1",
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
"zombie": "4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jenkins-cd/blueocean-core-js": "0.0.34",
|
||||
"@jenkins-cd/blueocean-core-js": "0.0.35",
|
||||
"@jenkins-cd/design-language": "0.0.91",
|
||||
"@jenkins-cd/js-extensions": "0.0.32",
|
||||
"@jenkins-cd/js-modules": "0.0.8",
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
<url>https://wiki.jenkins-ci.org/display/JENKINS/Blue+Ocean+Plugin</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>blueocean-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jenkins-ci.plugins</groupId>
|
||||
<artifactId>metrics</artifactId>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2016, CloudBees, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.jenkins.blueocean;
|
||||
|
||||
import hudson.Extension;
|
||||
import io.jenkins.blueocean.commons.PageStatePreloader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
|
||||
*/
|
||||
@Extension
|
||||
public class PageStatePreloadDecorator extends BluePageDecorator {
|
||||
|
||||
public List<PageStatePreloader> getPageStatePreloaders(){
|
||||
return PageStatePreloader.all();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
|
||||
<script>
|
||||
(function () {
|
||||
// construct the state object parent path inside window.$$blueocean.
|
||||
function setState(statePropertyPath, state) {
|
||||
var pathTokens = ('$blueocean.' + statePropertyPath).split('.');
|
||||
var contextObj = window;
|
||||
|
||||
// Basically an Array shift
|
||||
function nextToken() {
|
||||
var nextToken = pathTokens[0];
|
||||
pathTokens = pathTokens.slice(1);
|
||||
return nextToken;
|
||||
}
|
||||
|
||||
var pathToken = nextToken();
|
||||
|
||||
// Construct up to, but not including, the last point in the graph.
|
||||
while (pathTokens.length !== 0) {
|
||||
if (!contextObj[pathToken]) {
|
||||
contextObj[pathToken] = {};
|
||||
}
|
||||
contextObj = contextObj[pathToken];
|
||||
pathToken = nextToken();
|
||||
}
|
||||
// And set the state on the last object on the graph.
|
||||
contextObj[pathToken] = state;
|
||||
}
|
||||
|
||||
<j:forEach var="preloader" items="${it.pageStatePreloaders}">
|
||||
// State Preloader: ${preloader.class.name}
|
||||
<j:set var="stateJson" value="${preloader.stateJson}"/>
|
||||
<j:if test="${stateJson != null}">
|
||||
setState('${preloader.statePropertyPath}', ${stateJson});
|
||||
</j:if>
|
||||
</j:forEach>
|
||||
})();
|
||||
</script>
|
||||
</j:jelly>
|
Loading…
Reference in New Issue