Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JEP-234] Customizable header #5909

Merged
merged 33 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f905a3f
Jenkins header revamp proposal
imonteroperez Oct 4, 2021
63367c7
Use invokeStatic instead of Functions
imonteroperez Nov 10, 2021
774139b
Remove headerDocumentation
imonteroperez Nov 10, 2021
41b0c6d
Remove headerDocumentation
imonteroperez Nov 10, 2021
29e3814
Simplification
imonteroperez Nov 11, 2021
88eac16
Some feedback from PR
imonteroperez Nov 11, 2021
c56b725
Merge branch 'header-revamp' of github.com:imonteroperez/jenkins into…
imonteroperez Nov 11, 2021
284abf7
Fix ident
imonteroperez Nov 11, 2021
ed961f3
Merge remote-tracking branch 'upstream/master' into header-revamp
imonteroperez Nov 12, 2021
8ba27fc
Fix conflicts
imonteroperez Nov 12, 2021
36692ce
Delete pageHeader.jelly.orig
imonteroperez Nov 12, 2021
ae94085
Move method get to Header
imonteroperez Nov 15, 2021
dfa8b38
Merge branch 'header-revamp' of github.com:imonteroperez/jenkins into…
imonteroperez Nov 15, 2021
72246ac
Remove extra space
imonteroperez Nov 15, 2021
4b45094
Setup visibility properly
imonteroperez Nov 15, 2021
6fe1552
Do not force to use ExtensionFilter
imonteroperez Nov 15, 2021
1dd8d32
Update get method
imonteroperez Nov 15, 2021
a173b2c
Update core/src/main/java/jenkins/views/Header.java
imonteroperez Nov 15, 2021
d634f6c
Update core/src/main/java/jenkins/views/JenkinsHeader.java
imonteroperez Nov 16, 2021
9131855
Merge remote-tracking branch 'upstream/master' into header-revamp
imonteroperez Nov 16, 2021
6c82f4a
Fix some idents
imonteroperez Nov 16, 2021
10e26c6
Deal with compatibility (partial/full) headers
imonteroperez Nov 16, 2021
87e741d
Fix build
imonteroperez Nov 16, 2021
82b68da
Use administrative monitor and use int instead of VersionNumber
imonteroperez Nov 16, 2021
68a6b9c
Remove tabs
imonteroperez Nov 16, 2021
f54c365
Apply suggestions from code review
imonteroperez Nov 17, 2021
2fa8ea7
Use jul
imonteroperez Nov 17, 2021
f0cb623
Update core/src/main/java/jenkins/views/JenkinsHeader.java
imonteroperez Nov 17, 2021
c8de3cb
Fix import
imonteroperez Nov 17, 2021
5ed6ad9
Add some javadoc to Header
imonteroperez Nov 19, 2021
fcbd5f0
Update core/src/main/java/jenkins/views/Header.java
imonteroperez Nov 19, 2021
e4ddcf1
Fix javadoc ref
imonteroperez Nov 19, 2021
f50b69c
Apply suggestions from code review
imonteroperez Nov 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions core/src/main/java/jenkins/views/FullHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package jenkins.views;

/**
* {@link Header} that provides its own resources as full replacement. It does not
* depends on any core resource (images, CSS, JS, etc.)
*
* Given this kind of header is totally independent, it will be compatible by default.
*
* @see Header
*/
public abstract class FullHeader extends Header {

public boolean isCompatible() {
return true;
}
}
60 changes: 60 additions & 0 deletions core/src/main/java/jenkins/views/Header.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package jenkins.views;

import java.util.Optional;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import hudson.ExtensionList;
import hudson.ExtensionPoint;

/**
* Extension point that provides capabilities to render a specific header.
*
* Extend {@link PartialHeader} or {@link FullHeader} depending on the use case.
*
* The default Jenkins header is provided as an implementation of a {@link FullHeader}
* named {@link JenkinsHeader}.
*
* All headers will provide a prioritization technique, via the usual ordinal field of
* the {@link Extension} annotation.
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
*
* The header content will be injected inside the pageHeader.jelly, based on the header
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
* retrieved by the {@link Header#get()} method. That header content will be provided
* inside a resource called headerContent.jelly. It performs a full replacement
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
* of the header.
*
* @see PartialHeader
* @see FullHeader
* @see JenkinsHeader
* @since TODO
*/
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
public abstract class Header implements ExtensionPoint {
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved

/**
* Checks if header is available
* @return if header is available
*/
public boolean isAvailable() {
return isCompatible() && isEnabled();
}

/**
* Checks API compatibility of the header
* @return if header is compatible
*/
public abstract boolean isCompatible();

/**
* Checks if header is enabled.
* @return if header is enabled
*/
public abstract boolean isEnabled();

@Restricted(NoExternalUse.class)
public static Header get() {
Optional<Header> header = ExtensionList.lookup(Header.class).stream().filter(Header::isAvailable).findFirst();
return header.orElseGet(() -> new JenkinsHeader());
}

}
18 changes: 18 additions & 0 deletions core/src/main/java/jenkins/views/JenkinsHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package jenkins.views;

import hudson.Extension;

/**
* Default {@link Header} provided by Jenkins
*
* @see Header
*/
@Extension(ordinal = Integer.MIN_VALUE)
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
public class JenkinsHeader extends FullHeader {
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved

@Override
public boolean isEnabled() {
return true;
}

}
49 changes: 49 additions & 0 deletions core/src/main/java/jenkins/views/PartialHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package jenkins.views;

import java.util.logging.Logger;

import hudson.ExtensionList;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.util.AdministrativeError;
import jenkins.model.Jenkins;

/**
* {@link Header} that relies on core resources (images, CSS, JS, etc.) to perform
* partial replacements.
*
* Given this kind of header is not independent, compatibility should be managed by the
* specific {@link Header} compatibility header version value
*
* @see Header
*/
public abstract class PartialHeader extends Header {

private static Logger LOGGER = Logger.getLogger(PartialHeader.class.getName());
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved

/**
* The current compatibility version of the Header API.
*
* Increment this number when an incompatible change is made to the header (like the search form API).
*/
private static final int compatibilityHeaderVersion = 1;

@Override
public final boolean isCompatible() {
return compatibilityHeaderVersion == getSupportedHeaderVersion();
}

/**
* @return the supported header version
*/
public abstract int getSupportedHeaderVersion();
Comment on lines +31 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's too restrictive to force "equal" for the compatibility. No strong opinion


@Initializer(after = InitMilestone.JOB_LOADED, before = InitMilestone.JOB_CONFIG_ADAPTED)
@SuppressWarnings("unused")
public static void incompatibleHeaders() {
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
ExtensionList.lookup(PartialHeader.class).stream().filter(h -> !h.isCompatible()).forEach(header -> {
LOGGER.warning(String.format("%s:%s not compatible with %s", header.getClass().getName(), header.getSupportedHeaderVersion(), compatibilityHeaderVersion));
new AdministrativeError(header.getClass().getName(), "Incompatible Header", String.format("The plugin %s is attempting to replace the Jenkins header but is not compatible with this version of Jenkins. The plugin should be updated or removed.", Jenkins.get().getPluginManager().whichPlugin(header.getClass())), null);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:i="jelly:fmt" xmlns:x="jelly:xml">
<header id="header" class="page-header">
<div class="page-header__brand">
<div class="logo">
<a id="jenkins-home-link" href="${rootURL}/">
<img id="jenkins-head-icon" src="${imagesURL}/svgs/logo.svg" alt="[${logoAlt}]" />
<img id="jenkins-name-icon" src="${imagesURL}/title.svg" alt="${title}" width="139" height="34" />
</a>
</div>

<a class="page-header__brand-link" href="${rootURL}/">
<img src="${imagesURL}/svgs/logo.svg"
alt="[${logoAlt}]"
class="page-header__brand-image" />
<span class="page-header__brand-name">Jenkins</span>
</a>
</div>

<div class="searchbox hidden-xs">
imonteroperez marked this conversation as resolved.
Show resolved Hide resolved
<!-- search box -->
<j:set var="searchURL" value="${h.searchURL}"/>
<form action="${searchURL}" method="get" style="position:relative;" class="no-json" name="search" role="search">
<!-- this div is used to calculate the width of the text box -->
<div id="search-box-sizer"/>
<div id="searchform">
<input name="q" placeholder="${searchPlaceholder}" id="search-box" class="main-search__input" value="${request.getParameter('q')}" role="searchbox" />

<span class="main-search__icon-leading">
<l:svgIcon href="${resURL}/images/material-icons/svg-sprite-action-symbol.svg#ic_search_24px" />
</span>
<a href="${searchHelpUrl}" class="main-search__icon-trailing">
<l:svgIcon href="${resURL}/images/material-icons/svg-sprite-action-symbol.svg#ic_help_outline_24px" />
</a>

<div id="search-box-completion" />
<script>createSearchBox("${searchURL}");</script>
</div>
</form>
</div>

<div class="login page-header__hyperlinks">
<div id="visible-am-insertion" class="page-header__am-wrapper" />
<div id="visible-sec-am-insertion" class="page-header__am-wrapper" />

<!-- login field -->
<j:if test="${app.useSecurity}">
<j:choose>
<j:when test="${!h.isAnonymous()}">
<j:invokeStatic var="user" className="hudson.model.User" method="current" />
<j:choose>
<j:when test="${user.fullName == null || user.fullName.trim().isEmpty()}">
<j:set var="userName" value="${user.id}"/>
</j:when>
<j:otherwise>
<j:set var="userName" value="${user.fullName}"/>
</j:otherwise>
</j:choose>
<a href="${rootURL}/${user.url}" class="model-link inside inverse">
<l:svgIcon
class="am-monitor-icon" >
<use href="${resURL}/images/material-icons/svg-sprite-social-symbol.svg#ic_person_24px"></use>
</l:svgIcon>
<span class="hidden-xs hidden-sm">${userName}</span>
</a>
<j:if test="${app.securityRealm.canLogOut()}">
<a href="${rootURL}/logout">
<l:svgIcon href="${resURL}/images/material-icons/svg-sprite-action-symbol.svg#ic_input_24px" />
<span class="hidden-xs hidden-sm">${logout}</span>
</a>
</j:if>
</j:when>
<j:otherwise>
<st:include it="${app.securityRealm}" page="loginLink.jelly" />
</j:otherwise>
</j:choose>
</j:if>
</div>
</header>
</j:jelly>
80 changes: 2 additions & 78 deletions core/src/main/resources/lib/layout/pageHeader.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -25,82 +25,6 @@
Text for the logout link
</st:attribute>
</st:documentation>

<header id="header" class="page-header">
<div class="page-header__brand">
<div class="logo">
<a id="jenkins-home-link" href="${rootURL}/">
<img id="jenkins-head-icon" src="${imagesURL}/svgs/logo.svg" alt="[${logoAlt}]" />
<img id="jenkins-name-icon" src="${imagesURL}/title.svg" alt="${title}" width="139" height="34" />
</a>
</div>

<a class="page-header__brand-link" href="${rootURL}/">
<img src="${imagesURL}/svgs/logo.svg"
alt="[${logoAlt}]"
class="page-header__brand-image" />
<span class="page-header__brand-name">Jenkins</span>
</a>
</div>

<div class="searchbox hidden-xs">
<!-- search box -->
<j:set var="searchURL" value="${h.searchURL}"/>
<form action="${searchURL}" method="get" style="position:relative;" class="no-json" name="search" role="search">
<!-- this div is used to calculate the width of the text box -->
<div id="search-box-sizer"/>
<div id="searchform">
<input name="q" placeholder="${searchPlaceholder}" id="search-box" class="main-search__input" value="${request.getParameter('q')}" role="searchbox" />

<span class="main-search__icon-leading">
<l:svgIcon href="${resURL}/images/material-icons/svg-sprite-action-symbol.svg#ic_search_24px" />
</span>
<a href="${searchHelpUrl}" class="main-search__icon-trailing">
<l:svgIcon href="${resURL}/images/material-icons/svg-sprite-action-symbol.svg#ic_help_outline_24px" />
</a>

<div id="search-box-completion" />
<script>createSearchBox("${searchURL}");</script>
</div>
</form>
</div>

<div class="login page-header__hyperlinks">
<div id="visible-am-insertion" class="page-header__am-wrapper" />
<div id="visible-sec-am-insertion" class="page-header__am-wrapper" />

<!-- login field -->
<j:if test="${app.useSecurity}">
<j:choose>
<j:when test="${!h.isAnonymous()}">
<j:invokeStatic var="user" className="hudson.model.User" method="current" />
<j:choose>
<j:when test="${user.fullName == null || user.fullName.trim().isEmpty()}">
<j:set var="userName" value="${user.id}"/>
</j:when>
<j:otherwise>
<j:set var="userName" value="${user.fullName}"/>
</j:otherwise>
</j:choose>
<a href="${rootURL}/${user.url}" class="model-link inside inverse">
<l:svgIcon
class="am-monitor-icon" >
<use href="${resURL}/images/material-icons/svg-sprite-social-symbol.svg#ic_person_24px"></use>
</l:svgIcon>
<span class="hidden-xs hidden-sm">${userName}</span>
</a>
<j:if test="${app.securityRealm.canLogOut()}">
<a href="${rootURL}/logout">
<l:svgIcon href="${resURL}/images/material-icons/svg-sprite-action-symbol.svg#ic_input_24px" />
<span class="hidden-xs hidden-sm">${logout}</span>
</a>
</j:if>
</j:when>
<j:otherwise>
<st:include it="${app.securityRealm}" page="loginLink.jelly" />
</j:otherwise>
</j:choose>
</j:if>
</div>
</header>
<j:invokeStatic var="header" className="jenkins.views.Header" method="get"/>
<st:include it="${header}" page="headerContent.jelly"/>
</j:jelly>