Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
5 changes: 5 additions & 0 deletions cookies.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost FALSE /jenkins FALSE 0 JSESSIONID.05c074da node01ocqegq00mpin7k03soapl07p5.node0
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class AppNavLink extends AbstractLink {
private String url;
private String label;
private Logo logo;
private String id;

private boolean external;

Expand Down Expand Up @@ -83,6 +84,15 @@ public void setLogo(Logo logo) {
this.logo = logo;
}

@Exported
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed

public String getId() {
return id;
}

@DataBoundSetter
public void setId(String id) {
this.id = id;
}

@Exported
@Override
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

This class is not needed. When the appnav button is visible it will fetch dynamically all links including the currently defined favorites for a user.

Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.jenkins.plugins.customizable_header;

import hudson.Extension;
import hudson.model.Item;
import hudson.model.User;
import hudson.plugins.favorite.Favorites;
import hudson.plugins.favorite.listener.FavoriteListener;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest2;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

@Extension(optional = true)
public class FavoriteIntegrationListener extends FavoriteListener {

private static final Logger LOGGER = Logger.getLogger(FavoriteIntegrationListener.class.getName());
private static final String FAVORITES_ID = "favorites-nav";

@Override
public void onAddFavourite(Item item, User user) {
LOGGER.info("DEBUG: onAddFavourite triggered for item " + item.getFullName() + " and user " + user.getId());
updateFavoritesNavLink(user, true);
// Trigger the favorite status refresh event for UI
triggerAppNavRefresh();
}

@Override
public void onRemoveFavourite(Item item, User user) {
LOGGER.info("DEBUG: onRemoveFavourite triggered for item " + item.getFullName() + " and user " + user.getId());
// Check if the user has any favorites left
boolean hasFavorites = !isEmpty(Favorites.getFavorites(user));
LOGGER.info("DEBUG: User " + user.getId() + " has favorites: " + hasFavorites);
updateFavoritesNavLink(user, hasFavorites);
// Trigger the favorite status refresh event for UI
triggerAppNavRefresh();
}

private void updateFavoritesNavLink(User user, boolean hasFavorites) {
try {
// Get user header
UserHeader headerProp = user.getProperty(UserHeader.class);
if (headerProp == null) {
return;
}

List<AbstractLink> links = headerProp.getLinks();

// Find existing favorites link
AppNavLink favoritesLink = null;
for (AbstractLink link : links) {
if (link instanceof AppNavLink && FAVORITES_ID.equals(((AppNavLink)link).getId())) {
favoritesLink = (AppNavLink) link;
break;
}
}

if (hasFavorites) {
// Add favorites link if it doesn't exist and there are no other links
if (favoritesLink == null && links.isEmpty()) {
AppNavLink newLink = createFavoritesLink(user);
List<AbstractLink> newLinks = new ArrayList<>();
newLinks.add(newLink);
headerProp.setLinks(newLinks);
user.save();
LOGGER.fine("Added favorites link for user: " + user.getId());
}
} else {
// Remove favorites link if it exists and it's the only link
if (favoritesLink != null && links.size() == 1) {
headerProp.setLinks(new ArrayList<>());
user.save();
LOGGER.fine("Removed favorites link for user: " + user.getId());
}
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to update favorites link for user: " + user.getId(), e);
}
}

/**
* Triggers a refresh of the app-nav button visibility in the UI
* Called after favorite status changes to update the UI in real-time
*/
private void triggerAppNavRefresh() {
try {
// Use Jenkins.getInstance() to get the Jenkins instance
Jenkins jenkins = Jenkins.get();
if (jenkins != null) {
LOGGER.info("DEBUG: Triggering app-nav refresh after favorite change");
// Notify the frontend about the favorite status change
jenkins.getExtensionList(HeaderRootAction.class)
.get(0)
.notifyFavoriteStatusChanged();
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to trigger app-nav refresh after favorite change", e);
}
}

private AppNavLink createFavoritesLink(User user) {
// Create a new App Nav Link for favorites
AppNavLink link = new AppNavLink(
"user/" + user.getId() + "/favorites",
"Favorites",
new io.jenkins.plugins.customizable_header.logo.Symbol("symbol-star")
);
link.setId(FAVORITES_ID);
return link;
}

private boolean isEmpty(Iterable<?> iterable) {
return !iterable.iterator().hasNext();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Changes are not needed

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
Expand All @@ -28,6 +30,8 @@
*/
@Extension
public class HeaderRootAction implements UnprotectedRootAction {
private static final Logger LOGGER = Logger.getLogger(HeaderRootAction.class.getName());

@Override
public String getIconFileName() {
return null;
Expand Down Expand Up @@ -67,6 +71,43 @@
return CustomHeaderConfiguration.get().hasLinks();
}

/**
* Notifies the frontend about favorite status changes
* This method is called by the FavoriteIntegrationListener when a favorite is added or removed
*/
public void notifyFavoriteStatusChanged() {
LOGGER.info("DEBUG: notifyFavoriteStatusChanged called in HeaderRootAction");
// The actual notification is handled via the /favoriteStatusChanged endpoint
}

/**
* Endpoint for the frontend to check if there was a favorite status change
* @return JSON response indicating whether the app-nav button should be shown/hidden
*/
@GET
public FavoriteStatus doFavoriteStatusChanged() {
boolean hasLinks = hasLinks();
LOGGER.info("DEBUG: doFavoriteStatusChanged endpoint called, hasLinks=" + hasLinks);
return new FavoriteStatus(hasLinks);
}

/**
* Simple status object for favorite status changes
*/
@ExportedBean
public static class FavoriteStatus {
private final boolean showAppNav;

public FavoriteStatus(boolean showAppNav) {
this.showAppNav = showAppNav;
}

@Exported
public boolean isShowAppNav() {
return showAppNav;
}
}

@POST
public HttpResponse doAddSystemMessage(@QueryParameter(fixEmpty = true) String message, @QueryParameter(fixEmpty = true) String level,
@QueryParameter String expireDate, @QueryParameter(fixEmpty = true) String id, @QueryParameter(fixEmpty = true) Boolean dismissible) throws IOException {
Expand Down
Loading