diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 7426f11804..30f8de7f9d 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -89,6 +89,7 @@ import java.util.Set; import java.util.SortedMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import jenkins.model.BuildDiscarder; import jenkins.model.BuildDiscarderProperty; import jenkins.model.DirectlyModifiableTopLevelItemGroup; @@ -98,6 +99,12 @@ import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ModelObjectWithChildren; import jenkins.model.PeepholePermalink; import jenkins.model.ProjectNamingStrategy; +import jenkins.model.Tab; +import jenkins.model.details.Detail; +import jenkins.model.details.DetailFactory; +import jenkins.model.details.DownstreamProjectsDetail; +import jenkins.model.details.ProjectNameDetail; +import jenkins.model.details.UpstreamProjectsDetail; import jenkins.model.lazy.LazyBuildMixIn; import jenkins.scm.RunWithSCM; import jenkins.security.HexStringConfidentialKey; @@ -1689,4 +1696,30 @@ public abstract class Job, RunT extends Run { + + @Override + public Class type() { + return Job.class; + } + + @NonNull @Override public List createFor(@NonNull Job target) { + return Stream.of(new UpstreamProjectsDetail(target), new DownstreamProjectsDetail(target), new ProjectNameDetail(target)).filter(e -> e.getIconClassName() != null).toList(); + } + } + + /** + * Retrieves the tabs for a given job + */ + @Restricted(NoExternalUse.class) + public List getJobTabs() { + return getActions(Tab.class); + } + + @Restricted(NoExternalUse.class) + public final ParametersDefinitionProperty getParametersDefinitionProperty() { + return getProperty(ParametersDefinitionProperty.class); + } } diff --git a/core/src/main/java/jenkins/job/OverviewTab.java b/core/src/main/java/jenkins/job/OverviewTab.java new file mode 100644 index 0000000000..83fe43a58c --- /dev/null +++ b/core/src/main/java/jenkins/job/OverviewTab.java @@ -0,0 +1,50 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Jan Faracik + * + * 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 jenkins.job; + +import hudson.model.Actionable; +import jenkins.model.Tab; + +public class OverviewTab extends Tab { + + public OverviewTab(Actionable object) { + super(object); + } + + @Override + public String getIconFileName() { + return "symbol-overview"; + } + + @Override + public String getDisplayName() { + return "Overview"; + } + + @Override + public String getUrlName() { + return null; + } +} diff --git a/core/src/main/java/jenkins/job/OverviewTabFactory.java b/core/src/main/java/jenkins/job/OverviewTabFactory.java new file mode 100644 index 0000000000..3a02d3562d --- /dev/null +++ b/core/src/main/java/jenkins/job/OverviewTabFactory.java @@ -0,0 +1,55 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Jan Faracik + * + * 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 jenkins.job; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Job; +import java.util.Collection; +import java.util.Collections; +import jenkins.model.Tab; +import jenkins.model.TransientActionFactory; +import jenkins.model.experimentalflags.NewJobPageUserExperimentalFlag; + +@Extension(ordinal = Integer.MAX_VALUE) +public class OverviewTabFactory extends TransientActionFactory { + + @Override + public Class type() { + return Job.class; + } + + @NonNull + @Override + public Collection createFor(@NonNull Job target) { + boolean isExperimentalUiEnabled = new NewJobPageUserExperimentalFlag().getFlagValue(); + + if (!isExperimentalUiEnabled) { + return Collections.emptySet(); + } + + return Collections.singleton(new OverviewTab(target)); + } +} diff --git a/core/src/main/java/jenkins/model/details/DownstreamProjectsDetail.java b/core/src/main/java/jenkins/model/details/DownstreamProjectsDetail.java new file mode 100644 index 0000000000..363b852983 --- /dev/null +++ b/core/src/main/java/jenkins/model/details/DownstreamProjectsDetail.java @@ -0,0 +1,45 @@ +package jenkins.model.details; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.model.AbstractProject; +import hudson.model.Actionable; +import hudson.model.Item; +import java.util.List; + +/** + * Displays downstream projects of a project (if any) + */ +public class DownstreamProjectsDetail extends Detail { + + public DownstreamProjectsDetail(Actionable object) { + super(object); + } + + public @Nullable String getIconClassName() { + if (getProjects().isEmpty()) { + return null; + } + + return "symbol-arrow-down-circle-outline plugin-ionicons-api"; + } + + @Override + public @Nullable String getDisplayName() { + int projectSize = getProjects().size(); + + if (projectSize == 1) { + return "1 downstream project"; + } + + return projectSize + " downstream projects"; + } + + public List getProjects() { + if (!(getObject() instanceof AbstractProject)) { + return List.of(); + } + + List projects = ((AbstractProject) getObject()).getDownstreamProjects(); + return projects.stream().filter(e -> e.hasPermission(Item.READ)).toList(); + } +} diff --git a/core/src/main/java/jenkins/model/details/ProjectNameDetail.java b/core/src/main/java/jenkins/model/details/ProjectNameDetail.java new file mode 100644 index 0000000000..b131a73c2b --- /dev/null +++ b/core/src/main/java/jenkins/model/details/ProjectNameDetail.java @@ -0,0 +1,39 @@ +package jenkins.model.details; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.model.Actionable; +import hudson.model.Hudson; +import hudson.model.Job; +import java.util.Objects; + +/** + * Displays the full name of a project (if necessary) + */ +public class ProjectNameDetail extends Detail { + + public ProjectNameDetail(Actionable object) { + super(object); + } + + public @Nullable String getIconClassName() { + if (getDisplayName() == null) { + return null; + } + + return "symbol-information-circle"; + } + + @Override + public @Nullable String getDisplayName() { + var it = (Job) getObject(); + + if (Objects.equals(it.getFullName(), it.getFullDisplayName()) || it.getClass().getName().equals("MatrixConfiguration")) { + return null; + } + + boolean nested = it.getParent().getClass() != Hudson.class; + String label = nested ? "Full project name" : "Project name"; + + return label + ": " + it.getFullName(); + } +} diff --git a/core/src/main/java/jenkins/model/details/UpstreamProjectsDetail.java b/core/src/main/java/jenkins/model/details/UpstreamProjectsDetail.java new file mode 100644 index 0000000000..548ae5405f --- /dev/null +++ b/core/src/main/java/jenkins/model/details/UpstreamProjectsDetail.java @@ -0,0 +1,45 @@ +package jenkins.model.details; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.model.AbstractProject; +import hudson.model.Actionable; +import hudson.model.Item; +import java.util.List; + +/** + * Displays upstream projects of a project (if any) + */ +public class UpstreamProjectsDetail extends Detail { + + public UpstreamProjectsDetail(Actionable object) { + super(object); + } + + public @Nullable String getIconClassName() { + if (getProjects().isEmpty()) { + return null; + } + + return "symbol-arrow-up-circle-outline plugin-ionicons-api"; + } + + @Override + public @Nullable String getDisplayName() { + int projectSize = getProjects().size(); + + if (projectSize == 1) { + return "1 upstream project"; + } + + return projectSize + " upstream projects"; + } + + public List getProjects() { + if (!(getObject() instanceof AbstractProject)) { + return List.of(); + } + + List projects = ((AbstractProject) getObject()).getUpstreamProjects(); + return projects.stream().filter(e -> e.hasPermission(Item.READ)).toList(); + } +} diff --git a/core/src/main/java/jenkins/model/experimentalflags/NewJobPageUserExperimentalFlag.java b/core/src/main/java/jenkins/model/experimentalflags/NewJobPageUserExperimentalFlag.java new file mode 100644 index 0000000000..e560c7a7cd --- /dev/null +++ b/core/src/main/java/jenkins/model/experimentalflags/NewJobPageUserExperimentalFlag.java @@ -0,0 +1,49 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Jan Faracik + * + * 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 jenkins.model.experimentalflags; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +public class NewJobPageUserExperimentalFlag extends BooleanUserExperimentalFlag { + public NewJobPageUserExperimentalFlag() { + super("new-job-page.flag"); + } + + @Override + public String getDisplayName() { + return "New job page"; + } + + @Nullable + @Override + public String getShortDescription() { + return "Enables a revamped job page. This feature is still a work in progress, so some things might not work perfectly yet."; + } +} diff --git a/core/src/main/resources/hudson/model/AbstractProject/main.jelly b/core/src/main/resources/hudson/model/AbstractProject/main.jelly index 0fa78b2d07..c6d2bfc475 100644 --- a/core/src/main/resources/hudson/model/AbstractProject/main.jelly +++ b/core/src/main/resources/hudson/model/AbstractProject/main.jelly @@ -46,5 +46,8 @@ THE SOFTWARE. - + + + + diff --git a/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly b/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly index 27aa5e7463..89fea0d403 100644 --- a/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly +++ b/core/src/main/resources/hudson/model/AbstractProject/sidepanel.jelly @@ -35,23 +35,44 @@ THE SOFTWARE. - - - - - - - - - - - - + - + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Job/index.jelly b/core/src/main/resources/hudson/model/Job/index.jelly index d9b214900c..a8580b2349 100644 --- a/core/src/main/resources/hudson/model/Job/index.jelly +++ b/core/src/main/resources/hudson/model/Job/index.jelly @@ -24,44 +24,53 @@ THE SOFTWARE. - - - - -
-
- - - - - + + + + + + + + + + + +
+
+ + + + + + +

+ +

+
+
+ +
+
+ + + + + ${%Full project name}: ${it.fullName} + + + ${%Project name}: ${it.fullName} + + -

- -

-
-
- -
-
+ - - - - ${%Full project name}: ${it.fullName} - - - ${%Project name}: ${it.fullName} - - - - + + + - - - - - -
-
+ + + + +
diff --git a/core/src/main/resources/hudson/model/Job/new-job-page.jelly b/core/src/main/resources/hudson/model/Job/new-job-page.jelly new file mode 100644 index 0000000000..f44e129ef4 --- /dev/null +++ b/core/src/main/resources/hudson/model/Job/new-job-page.jelly @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/ParametersDefinitionProperty/contents.jelly b/core/src/main/resources/hudson/model/ParametersDefinitionProperty/contents.jelly new file mode 100644 index 0000000000..e56f6c649d --- /dev/null +++ b/core/src/main/resources/hudson/model/ParametersDefinitionProperty/contents.jelly @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + ${%Cancel} + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/ParametersDefinitionProperty/index.jelly b/core/src/main/resources/hudson/model/ParametersDefinitionProperty/index.jelly index 0bea62baf0..af356ca99d 100644 --- a/core/src/main/resources/hudson/model/ParametersDefinitionProperty/index.jelly +++ b/core/src/main/resources/hudson/model/ParametersDefinitionProperty/index.jelly @@ -40,37 +40,7 @@ THE SOFTWARE.

${it.job.pronoun} ${it.job.displayName}

${%description}

- - - - - - - - - - - - - - - ${%Cancel} - - - +
diff --git a/core/src/main/resources/jenkins/job/OverviewTab/index.jelly b/core/src/main/resources/jenkins/job/OverviewTab/index.jelly new file mode 100644 index 0000000000..0801131318 --- /dev/null +++ b/core/src/main/resources/jenkins/job/OverviewTab/index.jelly @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+
diff --git a/core/src/main/resources/jenkins/model/details/DownstreamProjectsDetail/detail.jelly b/core/src/main/resources/jenkins/model/details/DownstreamProjectsDetail/detail.jelly new file mode 100644 index 0000000000..ce1724da3d --- /dev/null +++ b/core/src/main/resources/jenkins/model/details/DownstreamProjectsDetail/detail.jelly @@ -0,0 +1,53 @@ + + + + + +
    + +
  • + + + + + + + +
  • +
    +
+
+ + +
diff --git a/core/src/main/resources/jenkins/model/details/UpstreamProjectsDetail/detail.jelly b/core/src/main/resources/jenkins/model/details/UpstreamProjectsDetail/detail.jelly new file mode 100644 index 0000000000..ac6de25d85 --- /dev/null +++ b/core/src/main/resources/jenkins/model/details/UpstreamProjectsDetail/detail.jelly @@ -0,0 +1,53 @@ + + + + + +
    + +
  • + + + + + + + +
  • +
    +
+
+ + +
diff --git a/core/src/main/resources/lib/hudson/project/configurable.jelly b/core/src/main/resources/lib/hudson/project/configurable.jelly index 907068bcfd..4d27827601 100644 --- a/core/src/main/resources/lib/hudson/project/configurable.jelly +++ b/core/src/main/resources/lib/hudson/project/configurable.jelly @@ -25,17 +25,21 @@ THE SOFTWARE. - + + + + - - + + - + - + - + + diff --git a/core/src/main/resources/lib/layout/job-subpage.jelly b/core/src/main/resources/lib/layout/job-subpage.jelly new file mode 100644 index 0000000000..d6fb0e1002 --- /dev/null +++ b/core/src/main/resources/lib/layout/job-subpage.jelly @@ -0,0 +1,131 @@ + + + + + + A reusable container for subpages relating to Jobs in Jenkins. + Renders a tabbed layout for jobs, including build status and actions. + + + + + + + + +
+
+ + + + + + +

+ +

+
+ + + +
+ +
+ +
+ + + + + + ${it.object.buildNowText} + + + + +
+ +
+
+ + +
+
+ + + +
+ + + + + ${%Configure} + + + + + ${%View Configuration} + + + + + + +
+ + +
+
+
+
+
+ +
+ +
+
+
+ + + + + + + +
+
+
diff --git a/core/src/main/resources/lib/layout/tabs.jelly b/core/src/main/resources/lib/layout/tabs.jelly index 9d6da04d0f..d5aa4b1ef1 100644 --- a/core/src/main/resources/lib/layout/tabs.jelly +++ b/core/src/main/resources/lib/layout/tabs.jelly @@ -32,20 +32,22 @@ THE SOFTWARE. - + + + diff --git a/src/main/scss/components/_dialogs.scss b/src/main/scss/components/_dialogs.scss index e9cd78c50b..c955da7e76 100644 --- a/src/main/scss/components/_dialogs.scss +++ b/src/main/scss/components/_dialogs.scss @@ -1,6 +1,8 @@ $jenkins-dialog-padding: 1.25rem; .jenkins-dialog { + --background: var(--dialog-background); + border-radius: 1rem; border: none; background-color: var(--dialog-background); diff --git a/src/main/scss/pages/_build.scss b/src/main/scss/pages/_build.scss index 262b1c23bc..98e01c1854 100644 --- a/src/main/scss/pages/_build.scss +++ b/src/main/scss/pages/_build.scss @@ -154,7 +154,7 @@ body:has(.app-build-bar) { min-height: 2.875rem; margin-top: -5px; padding-bottom: 0.75rem; - z-index: 2; + z-index: 102; &::before { content: ""; diff --git a/src/main/scss/pages/_job.scss b/src/main/scss/pages/_job.scss index 105817790a..808d12ddef 100644 --- a/src/main/scss/pages/_job.scss +++ b/src/main/scss/pages/_job.scss @@ -1,8 +1,12 @@ @use "../abstracts/mixins"; -#buildHistoryPage { - margin: 10px 0 10px 10px; +#side-panel { + #buildHistoryPage { + margin: 10px 0 10px 10px; + } +} +#buildHistoryPage { .jenkins-search { margin-inline: -0.25rem; margin-bottom: 5px;