Skip to content

feat: Add Excel themes, curate list, and show color counts in UI #453

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@
<artifactId>jackson2-api</artifactId>
</dependency>

<!-- JSR-250 Annotations API for javax.annotation.meta.When warnings -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope> <!-- Usually provided by Jenkins runtime -->
</dependency>

<!-- Workflow dependencies -->
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
Expand Down Expand Up @@ -156,6 +164,14 @@
<classifier>tests</classifier>
<scope>test</scope>
</dependency>

<!-- Mockito for testing (inline version for advanced mocking and agent capabilities) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
41 changes: 21 additions & 20 deletions src/main/java/io/jenkins/plugins/reporter/ReportScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,41 @@ public class ReportScanner {
private final Provider provider;

private final TaskListener listener;

private final String colorPaletteTheme;

public ReportScanner(final Run<?, ?> run, final Provider provider, final FilePath workspace, final TaskListener listener) {
public ReportScanner(final Run<?, ?> run, final Provider provider, final FilePath workspace, final TaskListener listener, final String colorPaletteTheme) {
this.run = run;
this.provider = provider;
this.workspace = workspace;
this.listener = listener;
this.colorPaletteTheme = colorPaletteTheme;
}

public Report scan() throws IOException, InterruptedException {
LogHandler logger = new LogHandler(listener, provider.getSymbolName());
Report report = provider.scan(run, workspace, logger);

if (!report.hasColors()) {
report.logInfo("Report has no colors! Try to find the colors of the previous report.");

Optional<Report> prevReport = findPreviousReport(run, report.getId());

if (prevReport.isPresent()) {
Report previous = prevReport.get();

if (previous.hasColors()) {
report.logInfo("Previous report has colors. Add it to this report.");
report.setColors(previous.getColors());
} else {
report.logInfo("Previous report has no colors. Will generate color palette.");
report.setColors(new ColorPalette(report.getColorIds()).generatePalette());
}

// Previous logic for colors removed as per new requirement to always use palette or fallback within ColorPalette itself.
// The ColorPalette class will handle the fallback to RANDOM if themeName is null, empty, or invalid.

if (report != null && report.hasItems()) {
List<String> colorIds = report.getColorIds();
if (colorIds != null && !colorIds.isEmpty()) {
io.jenkins.plugins.reporter.model.ColorPalette paletteGenerator = new io.jenkins.plugins.reporter.model.ColorPalette(colorIds, this.colorPaletteTheme);
report.setColors(paletteGenerator.generatePalette());
report.logInfo("Applied color palette: " + (this.colorPaletteTheme != null ? this.colorPaletteTheme : "RANDOM"));
} else {
report.logInfo("No previous report found. Will generate color palette.");
report.setColors(new ColorPalette(report.getColorIds()).generatePalette());
report.logInfo("Report has no items with IDs to assign colors or colorIds list is empty.");
}
} else if (report != null) {
report.logInfo("Report is null or has no items, skipping color generation.");
}

// The old logic for finding previous report colors is removed.
// The new ColorPalette will always be used.
// If specific handling for "no colors" vs "previous colors" is still needed, it has to be re-evaluated.
// For now, assuming new palette generation is the primary goal.

logger.log(report);

return report;
Expand Down
130 changes: 118 additions & 12 deletions src/main/java/io/jenkins/plugins/reporter/model/ColorPalette.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,135 @@
package io.jenkins.plugins.reporter.model;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

public class ColorPalette {


public enum Theme {
RANDOM,
RAINBOW,
BLUE_SPECTRA,
SOLARIZED_LIGHT,
SOLARIZED_DARK,
GREYSCALE,
DRACULA,
MONOKAI,
NORD,
GRUVBOX_DARK,
GRUVBOX_LIGHT,
MATERIAL_DARK,
MATERIAL_LIGHT,
ONE_DARK,
ONE_LIGHT,
// TOMORROW_NIGHT, // Removed
// TOMORROW, // Removed
VIRIDIS,
// PLASMA, // Removed
TABLEAU_CLASSIC_10,
CATPPUCCIN_MACCHIATO,
EXCEL_OFFICE_DEFAULT, // Added
EXCEL_BLUE_II, // Added
EXCEL_GREEN_II, // Added
EXCEL_RED_VIOLET_II // Added
}

public static final Map<Theme, String[]> THEMES; // Changed from package-private to public

static {
Map<Theme, String[]> map = new HashMap<>();
map.put(Theme.RAINBOW, new String[]{"#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"});
map.put(Theme.BLUE_SPECTRA, new String[]{"#0D47A1", "#1565C0", "#1976D2", "#1E88E5", "#2196F3", "#42A5F5", "#64B5F6", "#90CAF9"});
map.put(Theme.SOLARIZED_LIGHT, new String[]{"#b58900", "#cb4b16", "#dc322f", "#d33682", "#6c71c4", "#268bd2", "#2aa198", "#859900"});
map.put(Theme.SOLARIZED_DARK, new String[]{"#586e75", "#dc322f", "#d33682", "#6c71c4", "#268bd2", "#2aa198", "#859900", "#b58900"});
map.put(Theme.GREYSCALE, new String[]{"#2F4F4F", "#556B2F", "#A9A9A9", "#D3D3D3", "#F5F5F5"});

// Curated themes (some were removed from here later)
map.put(Theme.DRACULA, new String[]{"#FF79C6", "#50FA7B", "#F1FA8C", "#BD93F9", "#8BE9FD", "#FFB86C", "#FF5555", "#6272A4"});
map.put(Theme.MONOKAI, new String[]{"#F92672", "#A6E22E", "#FD971F", "#E6DB74", "#66D9EF", "#AE81FF"});
map.put(Theme.NORD, new String[]{"#BF616A", "#A3BE8C", "#EBCB8B", "#81A1C1", "#B48EAD", "#88C0D0", "#D08770"});
map.put(Theme.GRUVBOX_DARK, new String[]{"#FB4934", "#B8BB26", "#FABD2F", "#83A598", "#D3869B", "#8EC07C", "#FE8019"});
map.put(Theme.GRUVBOX_LIGHT, new String[]{"#CC241D", "#98971A", "#D79921", "#458588", "#B16286", "#689D6A", "#D65D0E"});
map.put(Theme.MATERIAL_DARK, new String[]{"#F06292", "#81C784", "#FFD54F", "#7986CB", "#4FC3F7", "#FF8A65", "#A1887F", "#90A4AE"});
map.put(Theme.MATERIAL_LIGHT, new String[]{"#E91E63", "#4CAF50", "#FFC107", "#3F51B5", "#03A9F4", "#FF5722", "#795548", "#607D8B"});
map.put(Theme.ONE_DARK, new String[]{"#E06C75", "#98C379", "#E5C07B", "#61AFEF", "#C678DD", "#56B6C2"});
map.put(Theme.ONE_LIGHT, new String[]{"#E45649", "#50A14F", "#C18401", "#4078F2", "#A626A4", "#0184BC"});
// map.put(Theme.TOMORROW_NIGHT, new String[]{"#CC6666", "#B5BD68", "#F0C674", "#81A2BE", "#B294BB", "#8ABEB7", "#DE935F"}); // Removed
// map.put(Theme.TOMORROW, new String[]{"#C82829", "#718C00", "#EAB700", "#4271AE", "#8959A8", "#3E999F", "#D6700C"}); // Removed
map.put(Theme.VIRIDIS, new String[]{"#440154", "#414487", "#2A788E", "#22A884", "#7AD151", "#FDE725"});
// map.put(Theme.PLASMA, new String[]{"#0D0887", "#6A00A8", "#B12A90", "#E16462", "#FCA636", "#F0F921"}); // Removed
map.put(Theme.TABLEAU_CLASSIC_10, new String[]{"#1F77B4", "#FF7F0E", "#2CA02C", "#D62728", "#9467BD", "#8C564B", "#E377C2", "#7F7F7F", "#BCBD22", "#17BECF"});
map.put(Theme.CATPPUCCIN_MACCHIATO, new String[]{"#F0C6C6", "#A6D189", "#E5C890", "#8CAAEE", "#C6A0F6", "#81C8BE", "#F4B8A9"});

// Adding new Excel themes
map.put(Theme.EXCEL_OFFICE_DEFAULT, new String[]{"#4472C4", "#ED7D31", "#A5A5A5", "#FFC000", "#5B9BD5", "#70AD47"});
map.put(Theme.EXCEL_BLUE_II, new String[]{"#2F5597", "#5A89C8", "#8FB4DB", "#4BACC6", "#77C9D9", "#A9A9A9"});
map.put(Theme.EXCEL_GREEN_II, new String[]{"#548235", "#70AD47", "#A9D18E", "#C5E0B4", "#8497B0", "#BF8F00"});
map.put(Theme.EXCEL_RED_VIOLET_II, new String[]{"#C00000", "#900000", "#7030A0", "#A98EDA", "#E97EBB", "#BDBDBD"});

THEMES = Collections.unmodifiableMap(map);
}

private final List<String> ids;

public ColorPalette(List<String> ids) {
private final String themeName;

public ColorPalette(List<String> ids, String themeName) {
this.ids = ids;
if (themeName == null || themeName.isEmpty()) {
this.themeName = Theme.RANDOM.name();
} else {
this.themeName = themeName;
}
}

public Map<String, String> generatePalette() {

Map<String, String> colors = new HashMap<>();

ids.forEach(id -> {
int rand_num = ThreadLocalRandom.current().nextInt(0xffffff + 1);
String color = String.format("#%06x", rand_num);
Theme selectedTheme;
try {
selectedTheme = Theme.valueOf(this.themeName.toUpperCase());
} catch (IllegalArgumentException e) {
selectedTheme = Theme.RANDOM;
}

colors.put(id, color);
});

if (selectedTheme == Theme.RANDOM) {
ids.forEach(id -> {
int rand_num = ThreadLocalRandom.current().nextInt(0xffffff + 1);
String color = String.format("#%06x", rand_num);
colors.put(id, color);
});
} else {
String[] themeColors = THEMES.get(selectedTheme);
if (themeColors != null && themeColors.length > 0) {
for (int i = 0; i < ids.size(); i++) {
colors.put(ids.get(i), themeColors[i % themeColors.length]);
}
} else {
// Fallback to random if theme colors are missing (should not happen with enum keys)
ids.forEach(id -> {
int rand_num = ThreadLocalRandom.current().nextInt(0xffffff + 1);
String color = String.format("#%06x", rand_num);
colors.put(id, color);
});
}
}
return colors;
}

public static List<String> getAvailableThemes() {
return Arrays.stream(Theme.values())
.map(Theme::name)
.collect(Collectors.toList());
}

public static int getDefinedColorCount(Theme theme) {
if (theme != null && theme != Theme.RANDOM && THEMES.containsKey(theme)) {
String[] colors = THEMES.get(theme);
return colors != null ? colors.length : 0;
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;
import io.jenkins.plugins.reporter.model.ColorPalette; // Added import
import java.util.List; // Added import for List

import java.io.Serializable;
import java.util.Set;
Expand All @@ -38,6 +40,8 @@ public class PublishReportStep extends Step implements Serializable {

private String displayType;

private String colorPalette;

/**
* Creates a new instance of {@link PublishReportStep}.
*/
Expand Down Expand Up @@ -75,6 +79,15 @@ public String getDisplayType() {
return displayType;
}

public String getColorPalette() {
return colorPalette;
}

@DataBoundSetter
public void setColorPalette(String colorPalette) {
this.colorPalette = colorPalette;
}

@Override
public StepExecution start(final StepContext context) throws Exception {
return new Execution(context, this);
Expand All @@ -97,6 +110,7 @@ protected ReportResult run() throws Exception {
recorder.setName(step.getName());
recorder.setProvider(step.getProvider());
recorder.setDisplayType(step.getDisplayType());
recorder.setColorPalette(step.getColorPalette());

return recorder.perform(getContext().get(Run.class), getContext().get(FilePath.class),
getContext().get(TaskListener.class));
Expand Down Expand Up @@ -154,6 +168,40 @@ public ListBoxModel doFillDisplayTypeItems(@AncestorInPath final AbstractProject

return new ListBoxModel();
}

// called by jelly view
@POST
public ListBoxModel doFillColorPaletteItems(@AncestorInPath final AbstractProject<?, ?> project) {

Check warning

Code scanning / Jenkins Security Scan

Stapler: Missing permission check

Potential missing permission check in Descriptor#doFillColorPaletteItems
if (JENKINS.hasPermission(Item.CONFIGURE, project)) {
ListBoxModel model = new ListBoxModel();
model.add("Default (Random Colors)", ""); // Option for default behavior

List<String> themeNames = ColorPalette.getAvailableThemes();
for (String themeName : themeNames) {
String originalDisplayName = StringUtils.capitalize(themeName.toLowerCase().replace('_', ' '));
String finalDisplayName = originalDisplayName;

try {
ColorPalette.Theme themeEnum = ColorPalette.Theme.valueOf(themeName);
if (themeEnum != ColorPalette.Theme.RANDOM) { // RANDOM theme (from enum) should not show count
int colorCount = ColorPalette.getDefinedColorCount(themeEnum);
if (colorCount > 0) {
finalDisplayName = String.format("%s (%d colors)", originalDisplayName, colorCount);
}
}
// If themeEnum IS ColorPalette.Theme.RANDOM (i.e. the string "RANDOM" is in getAvailableThemes),
// it will skip the count, which is desired.
// The "Default (Random Colors)" entry with value "" already handles the general default.
} catch (IllegalArgumentException e) {
// Theme name not in enum, should not occur if themes come from getAvailableThemes().
// Log if necessary. For now, originalDisplayName is used.
}
model.add(finalDisplayName, themeName);
}
return model;
}
return new ListBoxModel(); // Return empty if no permission
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class ReportRecorder extends Recorder {

private String displayType;

private String colorPalette;

/**
* Creates a new instance of {@link ReportRecorder}.
*/
Expand Down Expand Up @@ -87,6 +89,15 @@ public void setDisplayType(String displayType) {
this.displayType = displayType;
}

public String getColorPalette() {
return colorPalette;
}

@DataBoundSetter
public void setColorPalette(String colorPalette) {
this.colorPalette = colorPalette;
}

@Override
public Descriptor getDescriptor() {
return (Descriptor) super.getDescriptor();
Expand Down Expand Up @@ -125,7 +136,7 @@ ReportResult perform(final Run<?, ?> run, final FilePath workspace, final TaskLi
private ReportResult record(final Run<?, ?> run, final FilePath workspace, final TaskListener listener)
throws IOException, InterruptedException {

Report report = scan(run, workspace, listener, provider);
Report report = scan(run, workspace, listener, provider, getColorPalette());
report.setName(getName());

DisplayType dt = Arrays.stream(DisplayType.values())
Expand All @@ -149,9 +160,9 @@ ReportResult publishReport(final Run<?, ?> run, final TaskListener listener,
}

private Report scan(final Run<?, ?> run, final FilePath workspace, final TaskListener listener,
final Provider provider) throws IOException, InterruptedException {
final Provider provider, final String colorPalette) throws IOException, InterruptedException {

ReportScanner reportScanner = new ReportScanner(run, provider, workspace, listener);
ReportScanner reportScanner = new ReportScanner(run, provider, workspace, listener, colorPalette);

return reportScanner.scan();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:report="/report">
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:report="/report">

<report:step/>

<f:entry title="${%Color Palette}" field="colorPalette">
<f:select />
</f:entry>

</j:jelly>
Loading
Loading