Redesign the Jenkins header (#10245)

* WB

* Push

* More responsive

* Update headerContent.jelly

* Push

* Update logo.jelly

* Update _page-header.scss

* Update _page-header.scss

* Update header

* Tidy up breadcrumbs

* Tidy up focus

* Update _breadcrumbs.scss

* Update configure.jelly

* push

* Add badges

* Tidy

* Push

* Update headerContent.jelly

* Update ManageJenkinsAction.java

* Update headerContent.jelly

* Working!

* WB

* Tidy up

* Fixes

* Update sidepanel.jelly

* Lint

* Tidy up

* Update ManageJenkinsAction.java

* Simplify

* Update _side-panel-tasks.scss

* Update _side-panel-tasks.scss

* Update UserAction.java

* Update Jenkins.java

* Add border to account image

* Tidy up avatars

* Update _side-panel-tasks.scss

* Init

* Tidy up

* Hide behind flag

* Update sidepanel.jelly

* Push

* Tidy up

* Update logo.jelly

* Accessibility

* Update _breadcrumbs-new.scss

* Fix dropdown theme

* Update _breadcrumbs.scss

* Update _header.scss

* Update ManageJenkinsAction.java

* Remove flag

* Tidy up

* Update with HeaderAction

* Revert "Update with HeaderAction"

This reverts commit 2ea0b1f867.

* Tidy

* Update RootAction.java

* Update _breadcrumbs.scss

* Push

* Update _header.scss

* Update _header.scss

* Fix invisible actions not actually being invisible, make avatar huge to please Tim

* Tidy

* Push

* Fix breadcrumbs + notification

* Update jumplist.jelly

* Getting there 🚀

* WB

* Update index.jelly

* Update headerContent.jelly

* Responsive

* Push

* Push

* Push

* Update index.js

* Push

* Tidy up

* Tidy up

* Tidy

* Update logo.jelly

* Delete NewHeaderUserExperimentalFlag.java

* Lint

* Update index.js

* Update index.js

* Fix some tests

* Update headerContent.jelly

* Update headerContent.jelly

* Remove bravo test - need to confirm this

* Update Security3349Test.java

* Update pom.xml

* Fix SpotBugs + i18n

* Add doc for header scroll, support prefers contrast

* Add overflow menu for actions, improve accessibility

* Update actions-overflow.js

* Fix tests + accessibility

* Fix JS

* Update breadcrumbs-overflow.js

* Update breadcrumbs-overflow.js

* Add breadcrumb menu on hover, fix issues on mobile

* Update _breadcrumbs.scss

* i18n

* Update pom.xml

* Add tab support for user dropdown

* Squashed commit of the following:

commit 847981ebcb
Merge: 0ea6dcff0e 23f2b9ef59
Author: Kris Stern <krisstern@outlook.com>
Date:   Wed Feb 26 09:16:52 2025 +0800

    Merge branch 'master' into add-groups-to-command-palette

commit 0ea6dcff0e
Merge: c0777dbe79 68425e2cd4
Author: Kris Stern <krisstern@outlook.com>
Date:   Wed Feb 26 01:06:33 2025 +0800

    Merge branch 'master' into add-groups-to-command-palette

commit c0777dbe79
Merge: 1638afe17e c37293c52d
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 24 13:40:35 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 1638afe17e
Merge: c987a9e536 b97764d3fd
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Feb 21 11:49:03 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit c987a9e536
Merge: f909eec0d4 16748f4413
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Feb 20 08:17:19 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit f909eec0d4
Merge: 85eedb7e88 217b0f5742
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Feb 19 16:12:45 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 85eedb7e88
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Feb 19 16:11:24 2025 +0000

    Move to Item

commit 8f4f117bac
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Feb 19 15:43:50 2025 +0000

    Tighten up animations + improve contrast

commit d7b7d6388d
Merge: 8750f7cb92 4fa61274f9
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Feb 19 08:50:45 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 8750f7cb92
Merge: 7b527340a2 a05c33f797
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Feb 18 21:42:31 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 7b527340a2
Merge: e2c133d128 3505fb3540
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 17 20:59:02 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit e2c133d128
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Feb 16 18:23:14 2025 +0000

    Update require-changelog-label.yml

commit d32a61c1ea
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Feb 16 17:11:18 2025 +0000

    Update _theme.scss

commit 42ecfcac5c
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Feb 16 17:10:48 2025 +0000

    Rename to Items

commit cc3779171a
Merge: 0f1cb2187c 2b9d4d62a6
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Feb 16 17:10:25 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 0f1cb2187c
Merge: 04dc6cd222 9474c89bf1
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Feb 12 20:42:01 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 04dc6cd222
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Feb 11 17:06:31 2025 +0000

    Reduce spacing a touch, fix icon spacing

commit 0ab3665587
Merge: 7c9e172b2f 848ac9b66a
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Feb 11 14:44:03 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 7c9e172b2f
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Feb 11 11:23:27 2025 +0000

    Update Messages.properties

commit ec6a5e5ee0
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Feb 11 08:51:16 2025 +0000

    Fix test

commit 14a64885a2
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 10 16:44:39 2025 +0000

    Tidy up

commit 46a9e5681a
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 10 16:28:15 2025 +0000

    Tidy

commit d7270b1fa4
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 10 16:06:04 2025 +0000

    Tidy

commit b2da3f8d39
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 10 16:03:37 2025 +0000

    Tidy up

commit b746fba008
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 10 15:53:53 2025 +0000

    Move to extensionpoint

commit 7827304ae1
Merge: cac127d119 d03a2e11c9
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Feb 10 15:45:28 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit cac127d119
Merge: add75bf6a9 e3e3c45270
Author: Tim Jacomb <21194782+timja@users.noreply.github.com>
Date:   Mon Jan 13 11:03:10 2025 +0000

    Merge branch 'jenkinsci:master' into add-groups-to-command-palette

commit add75bf6a9
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jan 11 19:02:52 2025 +0000

    Update _command-palette.scss

commit eb4073f4fb
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jan 11 18:59:37 2025 +0000

    Tidy up

commit 323e48fddf
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jan 11 18:47:34 2025 +0000

    Update Job.java

commit 3cbdfbc4b5
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jan 11 18:47:13 2025 +0000

    Update _command-palette.scss

commit 8fecf0d880
Merge: 428e826fcd 331c7685ca
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jan 11 18:45:30 2025 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 428e826fcd
Merge: 5657369d95 f1b6d31272
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Dec 16 20:53:15 2024 +0000

    Merge branch 'master' into add-groups-to-command-palette

commit 5657369d95
Merge: 26f17a277f 674d5085c3
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Dec 13 09:46:35 2024 +0000

    Merge branch 'add-icons-to-command-palette' into add-groups-to-command-palette

commit 674d5085c3
Merge: 809d2e6120 7020e80af8
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Dec 13 09:42:59 2024 +0000

    Merge branch 'master' into add-icons-to-command-palette

commit 26f17a277f
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 22:10:56 2024 +0000

    Update _command-palette.scss

commit 2b6ffc85f3
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 22:09:09 2024 +0000

    Init

commit 809d2e6120
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 21:37:47 2024 +0000

    Make iconXml private, rename to icon

commit 3d45ca7c39
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 21:29:27 2024 +0000

    Add group field

commit 80f24cbfdc
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 21:25:23 2024 +0000

    Init

commit 1b9faa8fb0
Merge: d6868c970a 26738449cd
Author: Tim Jacomb <timjacomb1@gmail.com>
Date:   Wed Dec 11 21:11:56 2024 +0000

    Merge branch 'add-icons-to-command-palette' of github.com:janfaracik/jenkins into add-icons-to-command-palette

commit d6868c970a
Author: Tim Jacomb <timjacomb1@gmail.com>
Date:   Wed Dec 11 21:11:41 2024 +0000

    Reword javadoc

commit 26738449cd
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 21:03:29 2024 +0000

    Implement IconSpec in IComputer

commit 57910109f3
Merge: 661f994783 05ed7560fd
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 20:23:58 2024 +0000

    Merge branch 'master' into add-icons-to-command-palette

commit 661f994783
Merge: 23570203ea dad5ef3266
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 20:15:22 2024 +0000

    Merge branch 'refine-command-palette' into add-icons-to-command-palette

commit 23570203ea
Merge: 436a02b9d3 788ae63c50
Author: Tim Jacomb <timjacomb1@gmail.com>
Date:   Wed Dec 11 16:35:43 2024 +0000

    Merge branch 'add-icons-to-command-palette' of github.com:janfaracik/jenkins into add-icons-to-command-palette

commit 436a02b9d3
Author: Tim Jacomb <timjacomb1@gmail.com>
Date:   Wed Dec 11 16:35:24 2024 +0000

    Add support for images

commit a3fdb3e0c7
Merge: ea67d6a554 d22cc2fa3c
Author: Tim Jacomb <timjacomb1@gmail.com>
Date:   Wed Dec 11 15:27:12 2024 +0000

    Merge branch 'master' into add-icons-to-command-palette

commit 788ae63c50
Merge: ea67d6a554 d22cc2fa3c
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Dec 11 10:00:04 2024 +0000

    Merge branch 'jenkinsci:master' into add-icons-to-command-palette

commit dad5ef3266
Merge: cc63c9c8e5 d22cc2fa3c
Author: Tim Jacomb <21194782+timja@users.noreply.github.com>
Date:   Wed Dec 11 09:07:05 2024 +0000

    Merge branch 'master' into refine-command-palette

commit ea67d6a554
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Dec 10 22:26:16 2024 +0000

    Update Search.java

commit a9aadbab30
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Dec 10 22:25:40 2024 +0000

    Revert "Update Search.java"

    This reverts commit 24837ea667.

commit 24837ea667
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Dec 10 21:59:03 2024 +0000

    Update Search.java

commit d43a8d3b2f
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Dec 10 21:54:23 2024 +0000

    Init

commit cc63c9c8e5
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Dec 10 21:37:09 2024 +0000

    Refine command palette

* Move logo

* Revert "Move logo"

This reverts commit 25647d6a04.

* Move actions to taglib

* Split logo from breadcrumbs

* Fix sticky app bar

* Update _page-header.scss

* Update HudsonTest.java

* Update _page-header.scss

* Move breadcrumb loading above setting mode to header

* Reduce header height

* Increase logo height

* Move getActions to Header

* Update header avatar with jenkins-avatar

* Squashed commit of the following:

commit 5060044fcd
Merge: 0ea3e49fa1 2fb523ffe3
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Mar 22 17:56:53 2025 +0000

    Merge branch 'master' into improve-tooltips-dropdowns

commit 0ea3e49fa1
Merge: 3dd0b9f421 a1f9d3e7e2
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Mar 22 12:24:49 2025 +0000

    Merge branch 'master' into improve-tooltips-dropdowns

commit 3dd0b9f421
Merge: 7f5f814aa5 73185b257d
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Mar 21 09:57:52 2025 +0000

    Merge branch 'master' into improve-tooltips-dropdowns

commit 7f5f814aa5
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Mar 20 10:26:32 2025 +0000

    Update _dropdowns.scss

commit e9eee3c0a4
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Mar 20 10:25:35 2025 +0000

    Update _theme.scss

commit ce11fd1fb3
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Mar 20 10:22:07 2025 +0000

    Init

* Delete idea files

* Update core/src/main/java/jenkins/views/Header.java

Co-authored-by: Markus Winter <m.winter@sap.com>

* Sort actions manually in header

* Update markup and CSS

* Handle that dodgy SVG messing up the label

* Fix new computer missing sidepanel

* Update core/src/main/resources/lib/layout/header/actions.jelly

Co-authored-by: Markus Winter <m.winter@sap.com>

* Update core/src/main/java/jenkins/views/Header.java

Co-authored-by: Markus Winter <m.winter@sap.com>

* Update core/src/main/resources/lib/layout/header/actions.jelly

Co-authored-by: Markus Winter <m.winter@sap.com>

* Update Header.java

---------

Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
Co-authored-by: Kris Stern <krisstern@outlook.com>
Co-authored-by: Markus Winter <m.winter@sap.com>
This commit is contained in:
Jan Faracik 2025-04-18 05:24:10 +01:00 committed by GitHub
parent ee56e8ffbf
commit c42ab43c49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 1051 additions and 989 deletions

View File

@ -27,6 +27,10 @@ package hudson.model;
import hudson.Extension;
import hudson.Util;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import jenkins.management.AdministrativeMonitorsDecorator;
import jenkins.management.Badge;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithContextMenu;
@ -40,11 +44,11 @@ import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.StaplerResponse2;
/**
* Adds the "Manage Jenkins" link to the top page.
* Adds the "Manage Jenkins" link to the navigation bar.
*
* @author Kohsuke Kawaguchi
*/
@Extension(ordinal = 100) @Symbol("manageJenkins")
@Extension(ordinal = 998) @Symbol("manageJenkins")
public class ManageJenkinsAction implements RootAction, StaplerFallback, ModelObjectWithContextMenu {
@Override
public String getIconFileName() {
@ -88,4 +92,29 @@ public class ManageJenkinsAction implements RootAction, StaplerFallback, ModelOb
// If neither is the case, rewrite the relative URL to point to inside the /manage/ URL space
menu.add("manage/" + url, icon, iconXml, text, post, requiresConfirmation, badge, message);
}
@Override
public Badge getBadge() {
Jenkins jenkins = Jenkins.get();
AdministrativeMonitorsDecorator decorator = jenkins.getExtensionList(PageDecorator.class)
.get(AdministrativeMonitorsDecorator.class);
if (decorator == null) {
return null;
}
Collection<AdministrativeMonitor> activeAdministrativeMonitors = Optional.ofNullable(decorator.getMonitorsToDisplay()).orElse(Collections.emptyList());
boolean anySecurity = activeAdministrativeMonitors.stream().anyMatch(AdministrativeMonitor::isSecurity);
if (activeAdministrativeMonitors.isEmpty()) {
return null;
}
int size = activeAdministrativeMonitors.size();
String tooltip = size > 1 ? Messages.ManageJenkinsAction_notifications(size) : Messages.ManageJenkinsAction_notification(size);
return new Badge(String.valueOf(size),
tooltip,
anySecurity ? Badge.Severity.DANGER : Badge.Severity.WARNING);
}
}

View File

@ -321,7 +321,7 @@ public class MyViewsProperty extends UserProperty implements ModifiableViewGroup
return Jenkins.get().getMyViewsTabBar();
}
@Extension @Symbol("myView")
@Symbol("myView")
public static class GlobalAction implements RootAction {
@Override

View File

@ -24,8 +24,10 @@
package hudson.model;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Extension;
import hudson.ExtensionPoint;
import jenkins.management.Badge;
/**
* Marker interface for actions that are added to {@link jenkins.model.Jenkins}.
@ -38,4 +40,14 @@ import hudson.ExtensionPoint;
* @since 1.311
*/
public interface RootAction extends Action, ExtensionPoint {
/**
* A {@link Badge} shown on the button for the action.
*
* @return badge or {@code null} if no badge should be shown.
* @since TODO
*/
default @CheckForNull Badge getBadge() {
return null;
}
}

View File

@ -28,7 +28,6 @@ import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.diagnosis.ReverseProxySetupMonitor;
import hudson.model.AdministrativeMonitor;
import hudson.model.ManageJenkinsAction;
import hudson.model.PageDecorator;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
@ -56,9 +55,6 @@ public class AdministrativeMonitorsDecorator extends PageDecorator {
public AdministrativeMonitorsDecorator() {
// otherwise this would be added to every internal context menu building request
ignoredJenkinsRestOfUrls.add("contextMenu");
// don't show here to allow admins to disable malfunctioning monitors via AdministrativeMonitorsDecorator
ignoredJenkinsRestOfUrls.add("configure");
}
@NonNull
@ -163,11 +159,6 @@ public class AdministrativeMonitorsDecorator extends PageDecorator {
return null;
}
// Don't show on Manage Jenkins
if (o instanceof ManageJenkinsAction) {
return null;
}
// don't show for some URLs served directly by Jenkins
if (o instanceof Jenkins) {
String url = a.getRestOfUrl();

View File

@ -2369,7 +2369,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
public SearchIndexBuilder makeSearchIndex() {
SearchIndexBuilder builder = super.makeSearchIndex();
this.actions.stream().filter(e -> e.getIconFileName() != null).forEach(action -> builder.add(new SearchItem() {
this.actions.stream().filter(e -> !(e.getIconFileName() == null || e.getUrlName() == null)).forEach(action -> builder.add(new SearchItem() {
@Override
public String getSearchName() {
return action.getDisplayName();

View File

@ -1,7 +1,7 @@
/*
* The MIT License
*
* Copyright (c) 2011, CloudBees, Inc.
* 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
@ -22,31 +22,29 @@
* THE SOFTWARE.
*/
package lib.hudson;
package jenkins.model.navigation;
import static org.junit.Assert.assertNotNull;
import hudson.model.InvisibleAction;
import hudson.Extension;
import hudson.model.RootAction;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
/**
* @author Kohsuke Kawaguchi
* TODO
*/
public class ActionsTest {
@Extension(ordinal = 999)
public class SearchAction implements RootAction {
@Rule
public JenkinsRule j = new JenkinsRule();
@Test
public void override() throws Exception {
assertNotNull(j.createWebClient().goTo("").getElementById("bravo"));
@Override
public String getIconFileName() {
return "symbol-search";
}
@TestExtension
public static class RootActionImpl extends InvisibleAction implements RootAction {
@Override
public String getDisplayName() {
return "Search";
}
@Override
public String getUrlName() {
return null;
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.navigation;
import static hudson.Functions.getAvatar;
import hudson.Extension;
import hudson.model.Action;
import hudson.model.RootAction;
import hudson.model.User;
import java.util.ArrayList;
import java.util.List;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Display the user avatar in the navigation bar.
* Provides a handy jumplist for common user actions.
*/
@Extension(ordinal = -1)
public class UserAction implements RootAction {
@Override
public String getIconFileName() {
User current = User.current();
if (current == null) {
return null;
}
return getAvatar(current, "96x96");
}
@Override
public String getDisplayName() {
User current = User.current();
if (current == null) {
return null;
}
return current.getFullName();
}
@Override
public String getUrlName() {
User current = User.current();
if (current == null) {
return null;
}
return current.getUrl();
}
@Restricted(NoExternalUse.class)
public User getUser() {
return User.current();
}
@Restricted(NoExternalUse.class)
public List<Action> getActions() {
User current = User.current();
if (User.current() == null) {
return null;
}
List<Action> actions = new ArrayList<>();
actions.addAll(current.getPropertyActions());
actions.addAll(current.getTransientActions());
return actions.stream().filter(e -> e.getIconFileName() != null).toList();
}
}

View File

@ -1,8 +1,17 @@
package jenkins.views;
import hudson.ExtensionComponent;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Action;
import hudson.model.RootAction;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import jenkins.model.Jenkins;
import org.jenkins.ui.icon.IconSpec;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@ -54,4 +63,28 @@ public abstract class Header implements ExtensionPoint {
return header.orElseGet(JenkinsHeader::new);
}
/**
* @return a list of {@link Action} to show in the header, defaults to {@link hudson.model.RootAction} extensions
*/
@Restricted(NoExternalUse.class)
public List<Action> getActions() {
// There's an issue where new actions (e.g. a new plugin installation) don't appear in the order
// of their ordinal annotation - to work around that we manually sort the list
Map<String, Double> rootActionsOrdinal = ExtensionList.lookup(RootAction.class)
.getComponents()
.stream()
.collect(Collectors.toMap(
c -> c.getInstance().getClass().getName(),
ExtensionComponent::ordinal
));
return Jenkins.get()
.getActions()
.stream()
.filter(e -> e.getIconFileName() != null || (e instanceof IconSpec is && is.getIconClassName() != null))
.sorted(Comparator.comparingDouble(
a -> rootActionsOrdinal.getOrDefault(a.getClass().getName(), Double.MAX_VALUE)
).reversed())
.toList();
}
}

View File

@ -25,7 +25,7 @@ public abstract class PartialHeader extends Header {
*
* Increment this number when an incompatible change is made to the header (like the search form API).
*/
private static final int compatibilityHeaderVersion = 1;
private static final int compatibilityHeaderVersion = 2;
@Override
public final boolean isCompatible() {

View File

@ -28,11 +28,6 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout title="${%Manage Jenkins}" permissions="${app.MANAGE_AND_SYSTEM_READ}">
<j:if test="${taskTags==null}">
<st:include page="sidepanel.jelly" it="${app}" />
<!-- no need for additional breadcrumb here as we're on an index page already including breadcrumb -->
</j:if>
<l:main-panel>
<l:app-bar title="${%Manage Jenkins}">
<l:search-bar placeholder="${%Search settings}" id="settings-search-bar" />
@ -52,58 +47,58 @@ THE SOFTWARE.
<h2 class="jenkins-section__title">${category.key.label}</h2>
${taskTags!=null and attrs.contextMenu!='false' ? taskTags.addHeader(category.key.label) : null}
<div class="jenkins-section__items">
<j:forEach var="m" items="${category.value}">
<j:if test="${m.iconFileName != null}">
<div class="jenkins-section__item">
<j:set var="alt" value="${icon.replaceAll('\\d*\\.[^.]+$', '')}"/>
<j:set var="iconSrc" value="${h.tryGetIconPath(m.iconFileName, context)}"/>
<j:set var="sure" value="${%sure}"/>
<j:set var="iconXml">
<l:icon src="${m.iconFileName}" />
</j:set>
${taskTags!=null and attrs.contextMenu!='false' ? it.addContextMenuItem(taskTags, m.urlName, iconSrc, iconXml, m.displayName, m.requiresPOST, m.requiresConfirmation, m.badge, sure) : null}
<j:choose>
<j:when test="${m.requiresConfirmation}">
<l:confirmationLink href="${m.urlName}" post="${m.requiresPOST}" message="${%sure}" title="${m.displayName}">
<div class="jenkins-section__item__icon" aria-hidden="true">
<l:icon src="${m.iconFileName}" class="icon" />
</div>
<dl>
<dt>${m.displayName}</dt>
<dd><j:out value="${m.description}"/></dd>
<dd><st:include it="${m}" page="info.jelly" optional="true"/></dd>
</dl>
</l:confirmationLink>
</j:when>
<j:when test="${m.requiresPOST}">
<f:link href="${m.urlName}" post="${m.requiresPOST}">
<div class="jenkins-section__item__icon" aria-hidden="true">
<l:icon src="${m.iconFileName}" class="icon" />
</div>
<dl>
<dt>${m.displayName}</dt>
<dd><j:out value="${m.description}"/></dd>
<dd><st:include it="${m}" page="info.jelly" optional="true"/></dd>
</dl>
</f:link>
</j:when>
<j:otherwise>
<a href="${m.urlName}">
<div class="jenkins-section__item__icon" aria-hidden="true">
<l:icon src="${m.iconFileName}" class="icon" />
<l:badge badge="${m.badge}" class="jenkins-section__item__icon__badge"/>
</div>
<dl>
<dt>${m.displayName}</dt>
<dd><j:out value="${m.description}"/></dd>
<dd><st:include it="${m}" page="info.jelly" optional="true"/></dd>
</dl>
</a>
</j:otherwise>
</j:choose>
</div>
</j:if>
</j:forEach>
<j:forEach var="m" items="${category.value}">
<j:if test="${m.iconFileName != null}">
<div class="jenkins-section__item">
<j:set var="alt" value="${icon.replaceAll('\\d*\\.[^.]+$', '')}"/>
<j:set var="iconSrc" value="${h.tryGetIconPath(m.iconFileName, context)}"/>
<j:set var="sure" value="${%sure}"/>
<j:set var="iconXml">
<l:icon src="${m.iconFileName}" />
</j:set>
${taskTags!=null and attrs.contextMenu!='false' ? it.addContextMenuItem(taskTags, m.urlName, iconSrc, iconXml, m.displayName, m.requiresPOST, m.requiresConfirmation, m.badge, sure) : null}
<j:choose>
<j:when test="${m.requiresConfirmation}">
<l:confirmationLink href="${m.urlName}" post="${m.requiresPOST}" message="${%sure}" title="${m.displayName}">
<div class="jenkins-section__item__icon" aria-hidden="true">
<l:icon src="${m.iconFileName}" class="icon" />
</div>
<dl>
<dt>${m.displayName}</dt>
<dd><j:out value="${m.description}"/></dd>
<dd><st:include it="${m}" page="info.jelly" optional="true"/></dd>
</dl>
</l:confirmationLink>
</j:when>
<j:when test="${m.requiresPOST}">
<f:link href="${m.urlName}" post="${m.requiresPOST}">
<div class="jenkins-section__item__icon" aria-hidden="true">
<l:icon src="${m.iconFileName}" class="icon" />
</div>
<dl>
<dt>${m.displayName}</dt>
<dd><j:out value="${m.description}"/></dd>
<dd><st:include it="${m}" page="info.jelly" optional="true"/></dd>
</dl>
</f:link>
</j:when>
<j:otherwise>
<a href="${m.urlName}">
<div class="jenkins-section__item__icon" aria-hidden="true">
<l:icon src="${m.iconFileName}" class="icon" />
<l:badge badge="${m.badge}" class="jenkins-section__item__icon__badge"/>
</div>
<dl>
<dt>${m.displayName}</dt>
<dd><j:out value="${m.description}"/></dd>
<dd><st:include it="${m}" page="info.jelly" optional="true"/></dd>
</dl>
</a>
</j:otherwise>
</j:choose>
</div>
</j:if>
</j:forEach>
</div>
</section>
</j:forEach>

View File

@ -199,6 +199,8 @@ LabelExpression.LabelLink=<a href="{0}{2}">Label {1}</a> matches {3,choice,0#no
LabelExpression.NoMatch=No agent/cloud matches this label expression.
LabelExpression.NoMatch_DidYouMean=No agent/cloud matches this label expression. Did you mean {1} instead of {0}?
ManageJenkinsAction.DisplayName=Manage Jenkins
ManageJenkinsAction.notification={0} notification
ManageJenkinsAction.notifications={0} notifications
MultiStageTimeSeries.EMPTY_STRING=
ParametersDefinitionProperty.BuildButtonText=Build
Queue.AllNodesOffline=All nodes of label {0} are offline

View File

@ -60,7 +60,6 @@ THE SOFTWARE.
<st:include page="sidepanel2.jelly" optional="true"/>
<st:include page="tasks-bottom.jelly" it="${it.owner}" optional="true" />
<t:actions />
</l:tasks>
<j:forEach var="w" items="${it.widgets}">
<j:set var="view" value="${it}" /><!-- expose the view that's rendering this sidepanel to the widget -->

View File

@ -25,7 +25,4 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<st:include page="/hudson/security/SecurityRealm/loginLink.jelly" />
<j:if test="${it.allowsSignup()}">
<a href="${rootURL}/signup">${%sign up}</a>
</j:if>
</j:jelly>

View File

@ -24,6 +24,12 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<a href="${rootURL}/${app.securityRealm.loginUrl}?from=${app.securityRealm.from}">${%login}</a>
<j:jelly xmlns:j="jelly:core">
<a class="jenkins-button"
data-type="header-action"
href="${rootURL}/${app.securityRealm.loginUrl}?from=${app.securityRealm.from}"
style="aspect-ratio: unset; padding: 0.5rem 1rem"
tooltip="${%signInTooltip}">
${%signIn}
</a>
</j:jelly>

View File

@ -21,3 +21,5 @@
# THE SOFTWARE.
login=log in
signIn=Sign in
signInTooltip=Sign in to access and manage your Jenkins jobs

View File

@ -1,71 +0,0 @@
<!--
The MIT License
Copyright (c) 2016, Daniel Beck, Keith Zantow, 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.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<j:set var="activeMonitors" value="${it.getMonitorsToDisplay()}"/>
<j:if test="${activeMonitors != null and activeMonitors.size() > 0}">
<st:adjunct includes="jenkins.management.AdministrativeMonitorsDecorator.resources"/>
<j:set var="activeNonSecurityAM" value="${it.filterNonSecurityAdministrativeMonitors(activeMonitors)}" />
<j:set var="activeNonSecurityAMCount" value="${activeNonSecurityAM.size()}" />
<j:set var="activeSecurityAM" value="${it.filterSecurityAdministrativeMonitors(activeMonitors)}" />
<j:set var="activeSecurityAMCount" value="${activeSecurityAM.size()}" />
<div id="visible-am-container" class="am-container">
<j:if test="${activeNonSecurityAMCount > 0}">
<a id="visible-am-button"
class="am-button"
href="#"
data-href="${rootURL}/administrativeMonitorsApi/nonSecurityPopupContent"
title="${%tooltip(activeNonSecurityAMCount)}">
<l:icon src="symbol-notifications" class="icon-md"/>
<div class="am-monitor__indicator-mobile"/>
<span class="am-monitor__count">
${activeNonSecurityAMCount}
</span>
</a>
<div id="visible-am-list" class="am-list">
</div>
</j:if>
</div>
<div id="visible-sec-am-container" class="am-container">
<j:if test="${activeSecurityAMCount > 0}">
<a id="visible-sec-am-button"
class="am-button security-am"
href="#"
data-href="${rootURL}/administrativeMonitorsApi/securityPopupContent"
title="${%tooltipSec(activeSecurityAMCount)}">
<l:icon src="symbol-shield-warning" class="icon-md"/>
<div class="am-monitor__indicator-mobile"/>
<span class="am-monitor__count">
${activeSecurityAMCount}
</span>
</a>
<div id="visible-sec-am-list" class="am-list">
</div>
</j:if>
</div>
</j:if>
</j:jelly>

View File

@ -1,2 +0,0 @@
tooltip=There are {0} active administrative monitors.
tooltipSec=There are {0} active security administrative monitors.

View File

@ -1,27 +0,0 @@
# The MIT License
#
# Bulgarian translation: Copyright (c) 2017, Alexander Shopov <ash@kambanaria.org>
#
# 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.
# There are {0} active administrative monitors.
tooltip=\
В момента има {0} предупреждения.
Manage\ Jenkins=\
Управление на Jenkins

View File

@ -1,24 +0,0 @@
# The MIT License
#
# Copyright (c) 2017 Daniel Beck and a number of other of contributors
#
# 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.
Manage\ Jenkins=Jenkins verwalten
tooltip={0,choice,0#Keine Administrator-Warnungen sind|1#{0} Administrator-Warnung ist|1<{0} Administrator-Warnungen sind} aktiv.

View File

@ -1,2 +0,0 @@
tooltip=Il existe {0} moniteurs d''administration activés.
tooltipSec=Il existe {0} moniteurs d''administration de sécurité activés.

View File

@ -1,25 +0,0 @@
# The MIT License
#
# Italian localization plugin for Jenkins
# Copyright © 2020 Alessandro Menti
#
# 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.
Manage\ Jenkins=Gestisci Jenkins
tooltip=Ci sono {0} monitor amministrativi attivi.

View File

@ -1,24 +0,0 @@
# The MIT License
#
# Copyright (c) 2016-2017, Damian Szczepanik
#
# 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.
# There are {0} active administrative monitors.
tooltip=Znaleziono {0} aktywnych powiadomień dla administratorów
Manage\ Jenkins=Zarządzaj Jenkinsem

View File

@ -1,24 +0,0 @@
# The MIT License
#
# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors
#
# 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.
tooltip=Existem {0} monitores administrativos ativos.
tooltipSec=Existem {0} monitores administrativos de segurança ativos.

View File

@ -1,2 +0,0 @@
tooltip=Det finns {0} aktiva administrativa övervakningar.
tooltipSec=Det finns {0} aktiva säkerhetsadministrativa övervakningar.

View File

@ -1,25 +0,0 @@
# The MIT License
#
# Copyright (c) 2021, Mustafa Ulu
#
# 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.
Manage\ Jenkins=Jenkins''i Yönet
tooltip={0} adet aktif yönetimsel gösterge var.
tooltipSec={0} adet aktif yönetimsel güvenlik göstergesi var.

View File

@ -1,2 +0,0 @@
tooltip=有 {0} 個啟用中的管理監視器。
tooltipSec=有 {0} 個啟用中的安全性管理監視器。

View File

@ -1,184 +0,0 @@
.am-container {
display: contents;
}
.am-button {
position: relative;
}
.am-button .am-monitor__indicator-mobile {
display: none;
position: absolute;
top: 0.25rem;
right: 0.25rem;
border-radius: 50%;
width: 0.65rem;
height: 0.65rem;
background-color: var(--warning-color);
}
.security-am .am-monitor__indicator-mobile {
background-color: var(--error-color);
}
.am-button .am-monitor__count {
display: inline-flex;
justify-content: center;
align-items: center;
height: 18px;
min-width: 18px;
color: #fff;
background-color: var(--warning-color);
font-weight: var(--font-bold-weight);
font-size: var(--font-size-xs);
border-radius: 16px;
}
.am-button.security-am .am-monitor__count {
color: #fff;
background-color: var(--error-color);
}
.am-container div.am-list {
position: absolute;
top: 48px;
right: 2%;
height: auto;
padding: var(--section-padding);
text-align: left;
display: block;
background-color: var(--background);
box-shadow: var(--dropdown-box-shadow);
border-radius: 15px;
opacity: 0;
z-index: 0;
transform: scale(0);
}
.am-container.am-hidden div.am-list {
animation: hide-am-list 300ms ease-in 1 normal;
}
.am-container.visible div.am-list {
opacity: 1;
animation: show-am-list 300ms ease-in 1 normal forwards;
z-index: 1000;
}
@keyframes show-am-list {
from {
opacity: 0;
visibility: hidden;
transform: translateY(-10px) scale(0.975);
}
to {
opacity: 1;
visibility: visible;
transform: scale(1);
}
}
@keyframes hide-am-list {
from {
opacity: 1;
visibility: visible;
transform: scale(1);
z-index: 1000;
}
to {
opacity: 0;
visibility: hidden;
transform: translateY(-10px) scale(0.975);
z-index: 1000;
}
}
.am-container .am-message {
display: block;
line-height: 1.4em;
margin-bottom: 1.4em;
}
.am-container.visible .am-button:after {
background: var(--button-background--hover);
}
.am-message-list {
padding: 0;
margin: 0;
}
.am-container .am-message .alert form,
.am-container .am-message .jenkins-alert form {
position: relative;
float: right;
margin: -6px 0 0 0 !important;
gap: 0.5rem;
display: flex;
padding-left: 0.5rem;
}
.am-container .am-message .alert form span,
.am-container .am-message .jenkins-alert form span {
margin: 0 0 0 4px !important;
}
.am-container .am-message .alert,
.am-container .am-message .jenkins-alert {
margin-bottom: 0 !important;
}
.am-container .am-message dl dt::after {
content: ": ";
}
/* Restore hyperlink style overriden by the page header */
.am-container .am-list a:link {
display: inline-block;
color: var(--link-color);
text-decoration: underline;
margin-right: 0;
padding: 0;
font-weight: var(--link-font-weight);
}
.am-container .am-list a:visited {
color: var(--link-color);
}
.am-container .am-list a:hover,
.am-container .am-list a:focus,
.am-container .am-list a:active {
color: var(--link-color);
background-color: transparent;
text-decoration: underline;
text-decoration: var(--link-text-decoration--hover);
}
.am-container .am-list .jenkins-alert-success a {
color: var(--alert-success-text-color);
}
.am-container .am-list .jenkins-alert-info a {
color: var(--alert-info-text-color);
}
.am-container .am-list .jenkins-alert-warning a {
color: var(--alert-warning-text-color);
}
.am-container .am-list .jenkins-alert-danger a {
color: var(--alert-danger-text-color);
}
@media screen and (max-width: 576px) {
/* Hide non-security monitors on mobile view to avoid messing up the heading */
#visible-am-container {
display: none;
}
}
@media screen and (max-width: 768px) {
.am-button .am-monitor__indicator-mobile {
display: block;
}
.am-button .am-monitor__count {
display: none;
}
}

View File

@ -1,128 +0,0 @@
(function () {
function initializeAmMonitor(amMonitorRoot, options) {
var button = amMonitorRoot.querySelector(".am-button");
var amList = amMonitorRoot.querySelector(".am-list");
if (button === null || amList === null) {
return null;
}
var url = button.getAttribute("data-href");
function onClose(e) {
var list = amList;
var el = e.target;
while (el) {
if (el === list) {
return; // clicked in the list
}
el = el.parentElement;
}
close();
}
function onEscClose(e) {
var escapeKeyCode = 27;
if (e.keyCode === escapeKeyCode) {
close();
}
}
function show() {
if (options.closeAll) {
options.closeAll();
}
fetch(url).then((rsp) => {
if (rsp.ok) {
rsp.text().then((responseText) => {
var popupContent = responseText;
amList.innerHTML = popupContent;
amMonitorRoot.classList.add("visible");
amMonitorRoot.classList.remove("am-hidden");
document.addEventListener("click", onClose);
document.addEventListener("keydown", onEscClose);
// Applies all initialization code to the elements within the popup
// Among other things, this sets the CSRF crumb to the forms within
Behaviour.applySubtree(amList);
});
}
});
}
function close() {
if (amMonitorRoot.classList.contains("visible")) {
amMonitorRoot.classList.add("am-hidden");
}
amMonitorRoot.classList.remove("visible");
document.removeEventListener("click", onClose);
document.removeEventListener("keydown", onEscClose);
}
function toggle(e) {
if (amMonitorRoot.classList.contains("visible")) {
close();
} else {
show();
}
e.preventDefault();
}
function startListeners() {
button.addEventListener("click", toggle);
}
return {
close: close,
startListeners: startListeners,
};
}
document.addEventListener("DOMContentLoaded", function () {
var monitorWidgets;
function closeAll() {
monitorWidgets.forEach(function (widget) {
widget.close();
});
}
var normalMonitors = initializeAmMonitor(
document.getElementById("visible-am-container"),
{
closeAll: closeAll,
},
);
var securityMonitors = initializeAmMonitor(
document.getElementById("visible-sec-am-container"),
{
closeAll: closeAll,
},
);
monitorWidgets = [normalMonitors, securityMonitors].filter(
function (widget) {
return widget !== null;
},
);
monitorWidgets.forEach(function (widget) {
widget.startListeners();
});
});
})();
document.addEventListener("DOMContentLoaded", function () {
var amContainer = document.getElementById("visible-am-container");
var amInsertion = document.getElementById("visible-am-insertion");
if (amInsertion) {
amInsertion.appendChild(amContainer);
}
var secAmContainer = document.getElementById("visible-sec-am-container");
var secAmInsertion = document.getElementById("visible-sec-am-insertion");
if (secAmInsertion) {
secAmInsertion.appendChild(secAmContainer);
}
});

View File

@ -29,7 +29,7 @@ THE SOFTWARE.
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout permissions="${app.MANAGE_AND_SYSTEM_READ}" title="${%System}" type="one-column">
<st:include page="sidepanel.jelly" />
<f:breadcrumb-config-outline title="${%System}" />
<l:breadcrumb title="${%System}" />
<l:main-panel>
<j:set var="readOnlyMode" value="${!h.hasPermission(app.MANAGE)}"/>

View File

@ -0,0 +1,9 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout">
<div class="jenkins-keyboard-shortcut__tooltip">
<span class="jenkins-!-margin-right-1">
${%Search}
</span>
<l:keyboard-shortcut shortcut="CMD+K" />
</div>
</j:jelly>

View File

@ -0,0 +1,25 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:dd="/lib/layout/dropdowns">
<dd:custom>
<a class="jenkins-dropdown__item" href="${rootURL}/${it.user.url}">
<div class="jenkins-dropdown__item__icon">
<l:icon src="${it.iconFileName}" class="jenkins-avatar" />
</div>
${it.user.fullName}
</a>
</dd:custom>
<dd:separator />
<j:forEach var="action" items="${it.actions}">
<dd:item icon="${action.iconFileName}"
text="${action.displayName}"
href="${rootURL}/${it.user.url}/${action.urlName}" />
</j:forEach>
<dd:separator />
<dd:item icon="symbol-log-out"
text="${%Sign out}"
href="${rootURL}/logout" />
</j:jelly>

View File

@ -1,8 +1,20 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:h="/lib/layout/header">
<header id="page-header" class="page-header">
<h:logo/>
<h:searchbox/>
<h:login/>
</header>
<j:jelly xmlns:j="jelly:core" xmlns:h="/lib/layout/header" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<header id="page-header" class="jenkins-header">
<st:include page="prefix" optional="true" />
<div class="jenkins-header__main">
<div class="jenkins-header__navigation">
<st:include page="logo" />
<l:breadcrumbBar>
<j:out value="${breadcrumbs}" />
</l:breadcrumbBar>
</div>
</div>
<h:actions />
</header>
<script src="${resURL}/jsbundles/header.js" type="text/javascript" />
</j:jelly>

View File

@ -0,0 +1,7 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
<a href="${rootURL}/" class="app-jenkins-logo">
<img id="jenkins-head-icon" src="${imagesURL}/svgs/logo.svg" aria-hidden="true" />
<span class="jenkins-mobile-hide">Jenkins</span>
</a>
</j:jelly>

View File

@ -44,13 +44,13 @@ THE SOFTWARE.
<j:if test="${mode=='breadcrumbs'}">
<j:set var="hasLink" value="${attrs.href != null}" />
<li id="${attrs.id}" class="jenkins-breadcrumbs__list-item" aria-current="${hasLink ? null : 'page'}">
<li id="${attrs.id}" class="jenkins-breadcrumbs__list-item" data-type="breadcrumb-item" aria-current="${hasLink ? null : 'page'}">
<j:choose>
<j:when test="${!hasLink}">
${attrs.title}
<span>${attrs.title}</span>
</j:when>
<j:otherwise>
<a href="${attrs.href}" class="${attrs.hasMenu ? 'model-link' : ''}">
<a href="${attrs.href}" class="${attrs.hasMenu ? 'hoverable-model-link' : ''}">
${attrs.title}
</a>
</j:otherwise>

View File

@ -35,34 +35,22 @@ THE SOFTWARE.
]]>
</st:documentation>
<j:set var="contents" trim="true">
<d:invokeBody />
</j:set>
<div id="breadcrumbBar" class="jenkins-breadcrumbs" aria-label="breadcrumb">
<ol class="jenkins-breadcrumbs__list" id="breadcrumbs">
<j:forEach var="anc" items="${request2.ancestors}" indexVar="index">
<j:if test="${h.isModel(anc.object) and anc.prev.url!=anc.url}">
<j:set var="mode" value="breadcrumbs" />
<l:breadcrumb title="${anc.object == app ? '%Dashboard' : anc.object.displayName}"
href="${anc.url}/"
hasMenu="${h.isModelWithContextMenu(anc.object)}" />
<j:choose>
<j:when test="${h.isModelWithChildren(anc.object)}">
<li class="children" data-href="${anc.url}/">
<!-- shows '>' for rendering children -->
</li>
</j:when>
<j:otherwise>
<li class="separator">
</li>
</j:otherwise>
</j:choose>
<j:if test="${anc.object != app}">
<l:breadcrumb title="${anc.object.displayName}"
href="${anc.url}/"
hasMenu="${h.isModelWithContextMenu(anc.object)}" />
</j:if>
</j:if>
</j:forEach>
<!-- render additional breadcrumb items -->
<j:out value="${contents}" />
<d:invokeBody />
</ol>
</div>
</j:jelly>

View File

@ -0,0 +1,70 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:h="/lib/layout/header" xmlns:l="/lib/layout" xmlns:st="jelly:stapler" xmlns:x="jelly:xml">
<div class="jenkins-header__actions">
<st:include page="suffix" optional="true" />
<j:forEach var="action" items="${it.actions}">
<j:set var="isCurrent" value="${h.hyperlinkMatchesCurrentPage(action.urlName)}" />
<j:set var="jumplist">
<st:include it="${action}" page="jumplist.jelly" optional="true" />
</j:set>
<j:if test="${jumplist.length() == 0}">
<j:set var="tooltip">
<st:include it="${action}" page="tooltip.jelly" optional="true" />
</j:set>
</j:if>
<j:set var="badge" value="${action.badge}" />
<j:if test="${jumplist.length() == 0 and tooltip.length() == 0}">
<j:set var="tooltip">
<div style="text-align: center;">${action.displayName}</div>
<j:if test="${badge != null}">
<div style="text-align: center; color: var(--text-color-secondary)">${badge.tooltip}</div>
</j:if>
</j:set>
</j:if>
<j:set var="interactive" value="${jumplist.length() gt 0}" />
<j:set var="icon" value="${action.iconClassName != null ? action.iconClassName : action.iconFileName}"/>
<x:element name="${action.urlName == null ? 'button' : 'a'}">
<x:attribute name="data-dropdown">${interactive}</x:attribute>
<x:attribute name="id">root-action-${action.class.simpleName}</x:attribute>
<x:attribute name="href">${h.getActionUrl(app.url, action)}</x:attribute>
<j:if test="${interactive}">
<x:attribute name="data-tippy-offset">[0, 10]</x:attribute>
</j:if>
<j:if test="${!interactive}">
<x:attribute name="data-html-tooltip" escapeText="false"><j:out value="${tooltip}" /></x:attribute>
</j:if>
<x:attribute name="data-tooltip-interactive">${interactive}</x:attribute>
<x:attribute name="data-tippy-animation">tooltip</x:attribute>
<x:attribute name="data-tippy-theme">${interactive ? 'dropdown' : 'tooltip'}</x:attribute>
<x:attribute name="data-tippy-trigger">mouseenter focus</x:attribute>
<x:attribute name="data-tippy-touch">${interactive}</x:attribute>
<x:attribute name="data-type">header-action</x:attribute>
<x:attribute name="draggable">false</x:attribute>
<x:attribute name="class">jenkins-button ${isCurrent ? '' : 'jenkins-button--tertiary'}</x:attribute>
<l:icon src="${icon}" class="jenkins-avatar" />
<span class="jenkins-visually-hidden" data-type="action-label">${action.displayName}</span>
<j:if test="${badge != null}">
<span class="jenkins-badge jenkins-!-${badge.severity}-color" />
</j:if>
</x:element>
<j:if test="${interactive}">
<template>
<div class="jenkins-dropdown">
<j:out value="${jumplist}" />
</div>
</template>
</j:if>
<j:set var="jumplist" />
<j:set var="tooltip" />
</j:forEach>
<h:login/>
</div>
</j:jelly>

View File

@ -1,37 +1,8 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<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" />
<!-- user icon and login/logout links; only show if authentication is enabled and we're not handling a servlet error -->
<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">
<l:icon src="symbol-person-circle" class="icon-md"/>
<span class="hidden-xs hidden-sm">${userName}</span>
</a>
<j:if test="${app.securityRealm.canLogOut()}">
<a href="${rootURL}/logout">
<l:icon src="symbol-log-out" class="icon-md" />
<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:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler">
<j:if test="${app.useSecurity}">
<j:if test="${h.isAnonymous()}">
<st:include it="${app.securityRealm}" page="loginLink.jelly" />
</j:if>
</div>
</j:if>
</j:jelly>

View File

@ -135,6 +135,10 @@ THE SOFTWARE.
</l:hasPermission>
<meta name="ROBOTS" content="INDEX,NOFOLLOW" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<j:set var="breadcrumbs">
<j:set var="mode" value="breadcrumbs" />
<d:invokeBody />
</j:set>
<j:set var="mode" value="header" />
<d:invokeBody />
<j:if test="${extensionsAvailable}">
@ -149,7 +153,9 @@ THE SOFTWARE.
<script src="${resURL}/jsbundles/sortable-drag-drop.js" type="text/javascript"/>
<script src="${resURL}/jsbundles/app.js" type="text/javascript" defer="true" />
</head>
<body id="jenkins" class="${layoutType} jenkins-${h.version}" data-version="${h.version}" data-model-type="${it.class.name}">
<body id="jenkins" class="${layoutType} jenkins-${h.version}" data-version="${h.version}" data-model-type="${it.class.name}"
data-search-url="${rootURL + '/search/suggest'}"
data-search-help-url="${%searchBox.url}">
<l:command-palette />
<j:if test="${layoutType!='full-screen'}">
@ -161,11 +167,6 @@ THE SOFTWARE.
searchPlaceholder="${%search}"
searchHelpUrl="${%searchBox.url}"
logout="${%logout}"/>
<l:breadcrumbBar>
<j:set var="mode" value="breadcrumbs" />
<d:invokeBody/>
</l:breadcrumbBar>
</j:if>
<div id="page-body" class="app-page-body app-page-body--${layoutType} clear">

View File

@ -27,4 +27,5 @@
</st:documentation>
<j:invokeStatic var="header" className="jenkins.views.Header" method="get"/>
<st:include it="${header}" page="headerContent.jelly"/>
<script src="${resURL}/jsbundles/keyboard-shortcuts.js" type="text/javascript"/>
</j:jelly>

View File

@ -2,8 +2,7 @@
* @param {string} searchTerm
*/
function search(searchTerm) {
const address = document.getElementById("button-open-command-palette").dataset
.searchUrl;
const address = document.querySelector("body").dataset.searchUrl;
return fetch(`${address}?query=${encodeURIComponent(searchTerm)}`);
}

View File

@ -12,7 +12,7 @@ const datasources = [JenkinsSearchSource];
function init() {
const i18n = document.getElementById("command-palette-i18n");
const headerCommandPaletteButton = document.getElementById(
"button-open-command-palette",
"root-action-SearchAction",
);
if (headerCommandPaletteButton === null) {
return; // no JenkinsHeader, no h:searchbox
@ -67,7 +67,7 @@ function init() {
icon: Symbols.HELP,
type: "symbol",
label: i18n.dataset.getHelp,
url: headerCommandPaletteButton.dataset.searchHelpUrl,
url: document.querySelector("body").dataset.searchHelpUrl,
isExternal: true,
group: null,
}),

View File

@ -30,6 +30,43 @@ function generateJumplistAccessors() {
* Generates the dropdowns for the jump lists
*/
function generateDropdowns() {
behaviorShim.specify(
".hoverable-model-link",
"-hoverable-dropdown-",
1000,
(element) =>
Utils.generateDropdown(
element,
(instance) => {
const href = element.href;
if (element.items) {
instance.setContent(Utils.generateDropdownItems(element.items));
return;
}
fetch(Path.combinePath(href, "contextMenu"))
.then((response) => response.json())
.then((json) =>
instance.setContent(
Utils.generateDropdownItems(
mapChildrenItemsToDropdownItems(json.items),
),
),
)
.catch((error) => console.log(`Jumplist request failed: ${error}`))
.finally(() => (instance.loaded = true));
},
false,
{
trigger: "mouseenter",
offset: [-16, 10],
animation: "tooltip",
touch: false,
},
),
);
behaviorShim.specify(
"li.children, .jenkins-jumplist-link, #menuSelector, .jenkins-menu-dropdown-chevron",
"-dropdown-",

View File

@ -10,13 +10,21 @@ function init() {
"-dropdown-",
1000,
(element) => {
Utils.generateDropdown(element, (instance) => {
const elements =
element.nextElementSibling.content.children[0].children;
const mappedItems = Utils.convertHtmlToItems(elements);
Utils.generateDropdown(
element,
(instance) => {
const elements =
element.nextElementSibling.content.children[0].children;
const mappedItems = Utils.convertHtmlToItems(elements);
instance.setContent(Utils.generateDropdownItems(mappedItems));
});
instance.setContent(Utils.generateDropdownItems(mappedItems));
instance.loaded = true;
},
false,
{
appendTo: "parent",
},
);
},
);
}

View File

@ -1,6 +1,26 @@
import { createElementFromHtml } from "@/util/dom";
import { xmlEscape } from "@/util/security";
const hideOnPopperBlur = {
name: "hideOnPopperBlur",
defaultValue: true,
fn(instance) {
return {
onCreate() {
instance.popper.addEventListener("focusout", (event) => {
if (
instance.props.hideOnPopperBlur &&
event.relatedTarget &&
!instance.popper.contains(event.relatedTarget)
) {
instance.hide();
}
});
},
};
},
};
function dropdown() {
return {
content: "<p class='jenkins-spinner'></p>",
@ -11,6 +31,7 @@ function dropdown() {
arrow: false,
theme: "dropdown",
appendTo: document.body,
plugins: [hideOnPopperBlur],
offset: [0, 0],
animation: "dropdown",
duration: 250,

View File

@ -11,43 +11,50 @@ const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected";
* @param element - the element to generate the dropdown for
* @param callback - called to retrieve the list of dropdown items
*/
function generateDropdown(element, callback, immediate) {
function generateDropdown(element, callback, immediate, options = {}) {
if (element._tippy && element._tippy.props.theme === "dropdown") {
element._tippy.destroy();
}
tippy(
element,
Object.assign({}, Templates.dropdown(), {
hideOnClick:
element.dataset["hideOnClick"] !== "false" ? "toggle" : false,
onCreate(instance) {
const onload = () => {
if (instance.loaded) {
return;
}
document.addEventListener("click", (event) => {
const isClickInAnyDropdown =
!!event.target.closest("[data-tippy-root]");
const isClickOnReference = instance.reference.contains(
event.target,
);
if (!isClickInAnyDropdown && !isClickOnReference) {
instance.hide();
Object.assign(
{},
Templates.dropdown(),
{
hideOnClick:
element.dataset["hideOnClick"] !== "false" ? "toggle" : false,
onCreate(instance) {
const onload = () => {
if (instance.loaded) {
return;
}
});
callback(instance);
};
if (immediate) {
onload();
} else {
instance.reference.addEventListener("mouseenter", onload);
}
document.addEventListener("click", (event) => {
const isClickInAnyDropdown =
!!event.target.closest("[data-tippy-root]");
const isClickOnReference = instance.reference.contains(
event.target,
);
if (!isClickInAnyDropdown && !isClickOnReference) {
instance.hide();
}
});
callback(instance);
};
if (immediate) {
onload();
} else {
["mouseenter", "focus"].forEach((event) => {
instance.reference.addEventListener(event, onload);
});
}
},
},
}),
options,
),
);
}

View File

@ -0,0 +1,133 @@
import Utils from "@/components/dropdowns/utils";
import { createElementFromHtml } from "@/util/dom";
const OVERFLOW_ID = "jenkins-header-actions-overflow";
export default function computeActions() {
document
.querySelectorAll(
".jenkins-header__actions .jenkins-button[data-type='header-action'].jenkins-hidden",
)
.forEach((e) => {
e.classList.remove("jenkins-hidden");
});
if (!actionsOverflows()) {
removeOverflowButton();
return;
}
const items = [];
const actions = Array.from(
document.querySelectorAll(
".jenkins-header__actions .jenkins-button[data-type='header-action']",
),
).slice(1, -1);
const overflowButton = generateOverflowButton();
while (actionsOverflows()) {
const item = actions.pop();
if (!item) {
break;
}
items.unshift(item);
item.classList.add("jenkins-hidden");
}
Utils.generateDropdown(
overflowButton,
(instance) => {
const mappedItems = items.map((e) => {
let icon = e.querySelector("img");
if (icon) {
icon = icon.src;
}
let iconXml = e.querySelector("svg");
if (iconXml) {
icon = true;
iconXml = iconXml.outerHTML;
}
const span = e.querySelector("[data-type='action-label']");
let label = e.textContent;
if (span !== null) {
label = span.textContent;
}
return {
type: "link",
icon: icon,
iconXml: iconXml,
label: label,
url: e.href,
};
});
instance.setContent(Utils.generateDropdownItems(mappedItems));
},
true,
{
trigger: "mouseenter focus",
offset: [0, 10],
animation: "tooltip",
},
);
// We want to disable the User action href on touch devices so that they can still activate the overflow menu
const link = document.querySelector("#root-action-UserAction");
if (link) {
const originalHref = link.getAttribute("href");
const isTouchDevice = window.matchMedia("(hover: none)").matches;
// HTMLUnit doesn't register itself as supporting hover, thus the href is removed when it shouldn't be
if (isTouchDevice && !window.isRunAsTest) {
link.removeAttribute("href");
} else {
link.setAttribute("href", originalHref);
}
}
}
function actionsOverflows() {
const actions = document.querySelector(".jenkins-header__actions");
return actions.offsetWidth > Math.max(window.innerWidth / 4.5, 150);
}
function generateOverflowButton() {
// If an overflow menu already exists let's use that
const overflowMenu = document.querySelector("#" + OVERFLOW_ID);
if (overflowMenu) {
return overflowMenu;
}
// Generate an overflow menu to store actions
const element =
createElementFromHtml(`<button id="${OVERFLOW_ID}" class="jenkins-button jenkins-button--tertiary"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<circle cx="256" cy="256" r="45" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
<circle cx="441" cy="256" r="45" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
<circle cx="71" cy="256" r="45" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
</svg>
</button>`);
const actionsContainer = document.querySelector(".jenkins-header__actions");
// Insert the new element before the last child
actionsContainer.insertBefore(
element,
actionsContainer.children[actionsContainer.children.length - 2],
);
return element;
}
function removeOverflowButton() {
const overflowButton = document.querySelector("#" + OVERFLOW_ID);
if (overflowButton) {
overflowButton.remove();
}
}

View File

@ -0,0 +1,96 @@
import Utils from "@/components/dropdowns/utils";
import { createElementFromHtml } from "@/util/dom";
export default function computeBreadcrumbs() {
document
.querySelectorAll(".jenkins-breadcrumbs__list-item.jenkins-hidden")
.forEach((e) => {
e.classList.remove("jenkins-hidden");
});
if (!breadcrumbsBarOverflows()) {
removeOverflowButton();
return;
}
const items = [];
const breadcrumbs = Array.from(
document.querySelectorAll(`[data-type="breadcrumb-item"]`),
);
const breadcrumbsOverflow = generateOverflowButton().querySelector("button");
while (breadcrumbsBarOverflows()) {
const item = breadcrumbs.shift();
if (!item) {
break;
}
items.push(item);
item.classList.add("jenkins-hidden");
}
Utils.generateDropdown(
breadcrumbsOverflow,
(instance) => {
const mappedItems = items.map((e) => {
let href = e.querySelector("a");
if (href) {
href = href.href;
}
return {
type: "link",
label: e.textContent,
url: href,
};
});
instance.setContent(Utils.generateDropdownItems(mappedItems));
},
true,
{
trigger: "mouseenter focus",
offset: [0, 10],
animation: "tooltip",
},
);
}
function breadcrumbsBarOverflows() {
const breadcrumbsBar = document.querySelector("#breadcrumbBar");
return breadcrumbsBar.scrollWidth > breadcrumbsBar.offsetWidth;
}
function generateOverflowButton() {
// If an overflow menu already exists let's use that
const overflowMenu = document.querySelector(
".jenkins-breadcrumbs__list-item .jenkins-button",
);
if (overflowMenu) {
return overflowMenu.parentNode;
}
// Generate an overflow menu to store breadcrumbs
const logo = document.querySelector(".jenkins-breadcrumbs__list-item");
const element =
createElementFromHtml(`<li class="jenkins-breadcrumbs__list-item"><button class="jenkins-button jenkins-button--tertiary"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<circle cx="256" cy="256" r="45" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
<circle cx="441" cy="256" r="45" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
<circle cx="71" cy="256" r="45" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/>
</svg>
</button></li>`);
logo.after(element);
return element;
}
function removeOverflowButton() {
const breadcrumbsOverflow = document.querySelector(
".jenkins-breadcrumbs__list-item .jenkins-button",
);
if (breadcrumbsOverflow) {
breadcrumbsOverflow.parentNode.remove();
}
}

View File

@ -0,0 +1,66 @@
import computeActions from "@/components/header/actions-overflow";
import computeBreadcrumbs from "@/components/header/breadcrumbs-overflow";
function init() {
// Recompute what actions and breadcrumbs should be visible when the viewport size is changed
computeOverflow();
let lastWidth = window.innerWidth;
window.addEventListener("resize", () => {
if (window.innerWidth !== lastWidth) {
lastWidth = window.innerWidth;
computeOverflow();
}
});
// Fade in the page header on scroll, increasing opacity and intensity of the backdrop blur
window.addEventListener("scroll", () => {
const navigation = document.querySelector("#page-header");
const scrollY = Math.max(0, window.scrollY);
navigation.style.setProperty(
"--background-opacity",
Math.min(70, scrollY) + "%",
);
navigation.style.setProperty(
"--background-blur",
Math.min(40, scrollY) + "px",
);
if (
!document.querySelector(".jenkins-search--app-bar") &&
!document.querySelector(".app-page-body__sidebar--sticky")
) {
const prefersContrast = window.matchMedia(
"(prefers-contrast: more)",
).matches;
navigation.style.setProperty(
"--border-opacity",
Math.min(
prefersContrast ? 100 : 15,
prefersContrast ? scrollY * 3 : scrollY,
) + "%",
);
}
});
window.addEventListener("load", () => {
// We can't use :has due to HtmlUnit CSS Parser not supporting it, so
// these are workarounds for that same behaviour
if (document.querySelector(".jenkins-app-bar--sticky")) {
document
.querySelector(".jenkins-header")
.classList.add("jenkins-header--has-sticky-app-bar");
}
if (!document.querySelector(".jenkins-breadcrumbs__list-item")) {
document
.querySelector(".jenkins-header")
.classList.add("jenkins-header--no-breadcrumbs");
}
});
}
function computeOverflow() {
computeActions();
computeBreadcrumbs();
}
init();

View File

@ -5,6 +5,23 @@ const TOOLTIP_BASE = {
arrow: false,
theme: "tooltip",
animation: "tooltip",
touch: false,
popperOptions: {
modifiers: [
{
name: "preventOverflow",
options: {
boundary: "viewport",
padding:
parseFloat(
getComputedStyle(document.documentElement).getPropertyValue(
"--section-padding",
),
) * 16,
},
},
],
},
duration: 250,
};

View File

@ -2,7 +2,7 @@ import hotkeys from "hotkeys-js";
window.addEventListener("load", () => {
const openCommandPaletteButton = document.querySelector(
"#button-open-command-palette",
"#root-action-SearchAction",
);
if (openCommandPaletteButton) {
hotkeys(translateModifierKeysForUsersPlatform("CMD+K"), () => {

View File

@ -14,7 +14,7 @@ $colors: (
"yellow": oklch(80% 0.17 76),
"teal": oklch(60% 0.1122 216.72),
"white": #fff,
"black": oklch(from var(--accent-color) 2% 0.075 h),
"black": oklch(from var(--accent-color) 5% 0.075 h),
);
$semantics: (
"accent": var(--blue),
@ -71,16 +71,10 @@ $semantics: (
--background: var(--white);
// Header
--brand-link-color: var(--secondary);
--header-link-color: var(--white);
--header-bg-classic: var(--black);
--header-link-bg-classic-hover: #404040;
--header-link-bg-classic-active: #404040;
// Breadcrumbs bar
--breadcrumbs-bar-background: oklch(
from var(--text-color) 96.8% 0.005 h / 0.8
);
--header-background: var(--background);
--header-border: var(--text-color-secondary);
--header-color: var(--text-color);
--header-height: 4.125rem;
// App bar
--bottom-app-bar-shadow: color-mix(
@ -162,6 +156,7 @@ $semantics: (
}
@media (prefers-contrast: more) {
--header-border: var(--text-color);
--focus-input-border: var(--text-color);
--jenkins-border-color: var(--text-color);
--jenkins-border-color--subtle: var(--text-color);

View File

@ -1,28 +1,5 @@
@use "../base/breakpoints";
/* --------------- header --------------- */
#page-header .logo {
margin-left: 1.2rem;
display: inline-flex;
align-items: center;
position: relative;
height: 100%;
width: 100%;
}
#jenkins-home-link {
position: relative;
}
#jenkins-head-icon {
height: 2.5rem;
}
#jenkins-name-icon {
margin-left: 0.25rem;
}
/* -------------------------------------- */
.app-page-body {
@ -36,7 +13,7 @@
@media (min-width: breakpoints.$tablet-breakpoint) {
&--sticky {
position: sticky;
top: 44px;
top: var(--header-height);
align-self: flex-start;
}
}
@ -53,7 +30,8 @@
}
#main-panel {
padding: var(--section-padding);
padding: 0 var(--section-padding) var(--section-padding)
var(--section-padding);
display: inline-block;
width: 100%;
}

View File

@ -51,16 +51,14 @@
&--sticky {
position: sticky;
top: 40px;
padding-top: var(--section-padding);
margin-top: calc(var(--section-padding) * -1);
top: var(--header-height);
z-index: 2;
&::before,
&::after {
content: "";
position: absolute;
inset: 0 calc(var(--section-padding) * -1)
inset: calc(var(--header-height) * -1) calc(var(--section-padding) * -1)
calc(var(--section-padding) * -1);
z-index: -1;
pointer-events: none;
@ -70,14 +68,10 @@
background: var(--background);
mask-image: linear-gradient(black 70%, transparent);
opacity: 0.55;
@supports not (backdrop-filter: blur(15px)) {
opacity: 1;
}
}
&::after {
backdrop-filter: blur(15px);
backdrop-filter: blur(10px);
mask-image: linear-gradient(black 50%, transparent);
}
}

View File

@ -1,130 +1,91 @@
@use "../abstracts/mixins";
.jenkins-breadcrumbs {
position: sticky;
top: 0;
z-index: 999;
display: flex;
align-items: center;
padding: 0.55rem 0.7rem 0.55rem 0.75rem;
backdrop-filter: blur(15px);
overflow-x: auto;
background: var(--breadcrumbs-bar-background);
gap: 0.625rem;
margin-left: 0.625rem;
overflow: hidden;
&__list {
display: contents;
list-style-type: none;
& > * {
flex-shrink: 0;
z-index: 1;
}
&-item {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-color);
font-weight: normal;
font-size: var(--font-size-sm);
padding: 0.2rem 0.4rem;
display: contents;
a::before {
content: "";
position: absolute;
inset: -0.5rem -0.75rem !important;
pointer-events: all !important;
}
& > a {
@include mixins.item($border: false);
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: inherit;
font-size: inherit;
margin: 0;
padding: 0;
color: var(--text-color);
margin-right: 0 !important;
transition: var(--standard-transition);
--item-background: transparent;
--item-background--hover: transparent;
--item-background--active: transparent;
--item-box-shadow--focus: transparent;
transition: opacity var(--standard-transition);
&::before,
&::after {
inset: -0.25rem -0.6rem;
inset: -2px -6px;
border: none;
}
&:hover,
&:active,
&:focus,
&:focus-visible {
color: var(--text-color);
&:hover {
opacity: 0.7;
}
&:active {
opacity: 0.4;
}
}
& > .model-link {
@media (hover: none) {
margin-right: 30px !important;
}
&:hover,
&--open {
margin-right: 30px !important;
}
}
}
// '>' separator between two items
.children,
.separator {
position: relative;
width: 1rem;
height: 1rem;
margin: 0.375rem 0.2rem;
&::after {
content: "";
position: absolute;
inset: 0;
transition: var(--standard-transition);
background: var(--text-color-secondary);
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'%3E%3Ctitle%3EChevron Forward%3C/title%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='48' d='M184 112l144 144-144 144'/%3E%3C/svg%3E");
opacity: 0.6;
}
}
.separator {
&:last-of-type {
display: none;
}
}
.children {
cursor: pointer;
&:hover {
&::after {
opacity: 1;
transform: rotate(90deg);
}
& > a,
span {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 1rem;
font-size: var(--font-size-sm);
font-weight: var(--font-bold-weight);
margin: 0;
padding: 0;
color: inherit;
text-decoration: none;
text-wrap: nowrap;
flex-shrink: 0;
}
&:active {
&::after {
transform: translateY(2px) rotate(90deg);
opacity: 0.5;
}
}
&:hover,
&:active,
&:focus,
&:focus-visible {
&::after {
background: var(--text-color);
}
}
// Increase the hit target
// '/' separator between two items
&::before {
content: "";
position: absolute;
inset: -14px -5px;
background: transparent;
position: relative;
width: 1rem;
height: 1.25rem;
flex-shrink: 0;
mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url("data:image/svg+xml,%3Csvg width='10' height='20' viewBox='0 0 10 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 18L8 2' stroke='black' stroke-width='1.5px' stroke-linecap='round'/%3E%3C/svg%3E%0A");
background: color-mix(
in sRGB,
var(--text-color-secondary) 30%,
transparent
);
@media (prefers-contrast: more) {
background: var(--text-color);
}
}
}
}
@ -305,6 +266,7 @@
.jenkins-menu-dropdown-chevron {
&::after {
opacity: 0.5;
display: none;
}
}
}

View File

@ -10,7 +10,7 @@ $dropdown-padding: 0.375rem;
background: color-mix(in sRGB, var(--card-background) 85%, transparent);
backdrop-filter: var(--dropdown-backdrop-filter);
max-width: unset !important;
max-height: 75vh;
max-height: 60vh;
overflow-y: auto;
.tippy-content {

View File

@ -1,94 +1,147 @@
@use "../abstracts/mixins";
.page-header {
.jenkins-header {
--background-opacity: 0%;
--background-blur: 0;
--border-opacity: 0%;
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
height: 3.5rem;
gap: var(--section-padding);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
background-color: var(--header-bg-classic);
}
min-height: var(--header-height);
color: var(--header-color);
.page-header > * {
margin-right: 0.75rem;
}
.page-header__brand {
display: inline-block;
height: 3.5rem;
position: relative;
flex: 1; // push controls to the end of the block
}
// Need to use the element selector to increase weight otherwise it will be overriden by the
// a:visited selector if it is declared later
// Only styled by the overrides with the new UI enabled
a.page-header__brand-link {
display: none;
}
.page-header__brand-name {
color: inherit;
}
.page-header__brand-image {
height: 2rem;
width: 1.5rem;
margin-right: 0.75rem;
}
.page-header__am-wrapper {
display: contents;
}
.page-header__hyperlinks {
display: flex;
align-items: center;
}
.page-header__hyperlinks > a,
.page-header__hyperlinks > button,
.am-container > a {
@include mixins.item;
--text-color: var(--header-link-color);
display: inline-flex;
align-items: center;
appearance: none;
background: transparent;
outline: none;
border: none;
cursor: pointer;
color: var(--text-color);
text-decoration: none;
padding: 0.5rem;
margin-right: 0 !important;
svg {
width: 1.25rem;
height: 1.25rem;
&::before {
content: "";
position: absolute;
inset: 0;
backdrop-filter: blur(var(--background-blur));
background: color-mix(
in sRGB,
var(--header-background) var(--background-opacity),
transparent
);
border-bottom: var(--jenkins-border-width) solid
color-mix(
in sRGB,
var(--header-border) var(--border-opacity),
transparent
);
background-clip: padding-box;
z-index: -1;
}
&::before,
&::after {
inset: 0 !important;
}
.app-jenkins-logo {
@include mixins.item($border: false);
.jenkins-menu-dropdown-chevron {
position: relative;
top: unset !important;
right: unset !important;
margin-left: 0.5rem;
--item-background: transparent;
--item-background--hover: transparent;
--item-background--active: transparent;
--item-box-shadow--focus: transparent;
display: flex;
align-items: center;
justify-content: center;
align-self: center;
gap: 1rem;
color: var(--header-color);
transition: opacity var(--standard-transition);
&::before,
&::after {
opacity: 1;
inset: -2px -6px;
border: none;
}
&:hover {
opacity: 0.7;
}
&:active {
opacity: 0.4;
}
// Increase the hit-box for the logo for Fittss Law
&::before {
inset: -30px -1rem -1rem -100px !important;
pointer-events: all;
}
span {
font-family: Georgia, serif;
font-weight: 600;
font-size: 1.125rem;
}
#jenkins-head-icon {
height: 2.125rem;
margin-left: 0.15rem;
}
}
.jenkins-button {
min-width: 2.375rem;
min-height: 2.375rem;
padding: 0;
// For customizable-header-plugin
color: inherit !important;
svg {
width: 1.25rem;
height: 1.25rem;
}
img {
width: 1.625rem;
height: 1.625rem;
}
.jenkins-badge {
position: absolute;
top: calc(16%);
right: calc(16%);
min-width: 5px;
min-height: 5px;
padding: 0;
}
}
&__main {
display: grid;
grid-template-columns: 1fr auto;
padding-left: var(--section-padding);
width: 100%;
}
&__navigation {
display: grid;
grid-template-columns: auto 1fr;
align-items: stretch;
}
&__actions {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding-right: var(--section-padding);
}
&--has-sticky-app-bar {
&::before {
mask-image: linear-gradient(black 50%, transparent);
}
}
&--no-breadcrumbs {
.app-jenkins-logo span {
display: block !important;
}
}
}
.page-header__hyperlinks a span {
&:not(:first-child) {
margin-left: 0.25rem;
}
}

View File

@ -7,7 +7,7 @@ $background-outset: 0.7rem;
.subtasks {
display: flex;
flex-direction: column;
margin: var(--section-padding);
margin: 0 var(--section-padding) var(--section-padding) var(--section-padding);
gap: 0.125rem;
@media (min-width: breakpoints.$tablet-breakpoint) {
@ -26,15 +26,12 @@ $background-outset: 0.7rem;
#side-panel {
.jenkins-app-bar {
margin-top: var(--section-padding);
margin-left: var(--section-padding);
margin-right: var(--section-padding);
}
& > #tasks > .jenkins-search-container {
margin-left: -$background-outset;
margin-right: -$background-outset;
margin-bottom: calc(var(--section-padding) / 2);
margin: calc(var(--section-padding) * -0.5) #{-$background-outset} 1rem;
.jenkins-search__icon {
width: 2.8rem;
@ -47,10 +44,6 @@ $background-outset: 0.7rem;
}
}
#side-panel .jenkins-search__results-container--visible .task-link {
opacity: 0.3;
}
#tasks .task {
margin: 0 calc($background-outset * -1);
}

View File

@ -145,13 +145,13 @@ public class HudsonTest {
}
/**
* Top page should only have one item in the breadcrumb.
* Top page should have zero items in the breadcrumb.
*/
@Test
public void breadcrumb() throws Exception {
HtmlPage root = j.createWebClient().goTo("");
DomElement navbar = root.getElementById("breadcrumbs");
assertEquals(1, navbar.querySelectorAll(".jenkins-breadcrumbs__list-item").size());
assertEquals(0, navbar.querySelectorAll(".jenkins-breadcrumbs__list-item").size());
}
/**

View File

@ -52,13 +52,13 @@ public class Security3349Test {
assertEquals(403, adminViews.getWebResponse().getStatusCode());
HtmlPage adminUserPage = wc.goTo("user/admin/");
assertFalse(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));
assertFalse(adminUserPage.getVisibleText().contains("My Views"));
HtmlPage userViews = wc.goTo("user/user/my-views/view/all/");
assertEquals(200, userViews.getWebResponse().getStatusCode());
HtmlPage userUserPage = wc.goTo("user/user/");
assertTrue(userUserPage.getWebResponse().getContentAsString().contains("My Views"));
assertTrue(userUserPage.getVisibleText().contains("My Views"));
wc.login("admin");
@ -73,7 +73,7 @@ public class Security3349Test {
adminViews = wc.goTo("user/admin/my-views/view/all/");
assertEquals(200, adminViews.getWebResponse().getStatusCode());
adminUserPage = wc.goTo("user/admin/");
assertTrue(adminUserPage.getWebResponse().getContentAsString().contains("My Views"));
assertTrue(adminUserPage.getVisibleText().contains("My Views"));
}
}

View File

@ -67,7 +67,7 @@ public class SearchTest {
@Rule public JenkinsRule j = new JenkinsRule();
private void searchWithoutNavigating(HtmlPage page, String query) throws IOException {
HtmlButton button = page.querySelector("#button-open-command-palette");
HtmlButton button = page.querySelector("#root-action-SearchAction");
button.click();
HtmlInput search = page.querySelector("#command-bar");

View File

@ -1,6 +0,0 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
<div>
<b id='bravo'>Bravo</b>
</div>
</j:jelly>

View File

@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Person Circle</title><path fill="currentColor" d="M258.9 48C141.92 46.42 46.42 141.92 48 258.9c1.56 112.19 92.91 203.54 205.1 205.1 117 1.6 212.48-93.9 210.88-210.88C462.44 140.91 371.09 49.56 258.9 48zm126.42 327.25a4 4 0 01-6.14-.32 124.27 124.27 0 00-32.35-29.59C321.37 329 289.11 320 256 320s-65.37 9-90.83 25.34a124.24 124.24 0 00-32.35 29.58 4 4 0 01-6.14.32A175.32 175.32 0 0180 259c-1.63-97.31 78.22-178.76 175.57-179S432 158.81 432 256a175.32 175.32 0 01-46.68 119.25z"/><path fill="currentColor" d="M256 144c-19.72 0-37.55 7.39-50.22 20.82s-19 32-17.57 51.93C191.11 256 221.52 288 256 288s64.83-32 67.79-71.24c1.48-19.74-4.8-38.14-17.68-51.82C293.39 151.44 275.59 144 256 144z"/></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" stroke="none" d="M 259.24942 23.021637 C 128.221512 21.251862 21.253086 128.220276 23.022827 259.24823 C 24.770166 384.910919 127.090225 487.230988 252.75293 488.978302 C 383.803253 490.770447 490.749237 383.802032 488.957123 252.774109 C 487.232178 127.08902 384.912109 24.768951 259.24942 23.021637 Z M 400.850983 389.570496 C 399.947693 390.547302 398.656952 391.072144 397.328308 391.002899 C 395.999664 390.933685 394.770477 390.277466 393.973633 389.212036 C 383.955017 376.104187 371.685822 364.881744 357.73877 356.068665 C 329.221344 337.766418 293.08728 327.685608 256.00116 327.685608 C 218.915054 327.685608 182.781006 337.766418 154.263565 356.068665 C 140.317032 364.877808 128.047821 376.096497 118.028717 389.200867 C 117.231857 390.266296 116.002693 390.922485 114.674026 390.99173 C 113.345375 391.060944 112.054665 390.536102 111.151367 389.559265 C 78.284332 354.078979 59.666458 307.717773 58.86565 259.360229 C 57.039909 150.364441 146.478943 59.13327 255.519547 58.864441 C 364.56012 58.595642 453.136688 147.13858 453.136688 255.999969 C 453.174255 305.523621 434.498657 353.232788 400.850983 389.570496 Z"/>
<path fill="currentColor" stroke="none" d="M 256 144 C 236.279999 144 218.449997 151.390015 205.779999 164.820007 C 193.110001 178.25 186.779999 196.820007 188.210007 216.75 C 191.110001 256 221.520004 288 256 288 C 290.480011 288 320.829987 256 323.790009 216.76001 C 325.269989 197.02002 318.98999 178.619995 306.109985 164.940002 C 293.390015 151.440002 275.589996 144 256 144 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -29,6 +29,7 @@ module.exports = (env, argv) => ({
),
],
app: [path.join(__dirname, "src/main/js/app.js")],
header: [path.join(__dirname, "src/main/js/components/header/index.js")],
"pages/cloud-set": [
path.join(__dirname, "src/main/js/pages/cloud-set/index.js"),
path.join(__dirname, "src/main/js/pages/cloud-set/index.scss"),