diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da400d01e..271cee5909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +### Added +- Add tooltips to short links on repository overview ([#1441](https://github.com/scm-manager/scm-manager/pull/1441)) +- Show the date of the last commit for branches in the frontend ([#1439](https://github.com/scm-manager/scm-manager/pull/1439)) +- Unify and add description to key view across user settings ([#1440](https://github.com/scm-manager/scm-manager/pull/1440)) + ### Changed - Send mercurial hook callbacks over separate tcp socket instead of http ([#1416](https://github.com/scm-manager/scm-manager/pull/1416)) +## [2.10.1] - 2020-11-24 +### Fixed +- Improved logging of failures during plugin installation ([#1442](https://github.com/scm-manager/scm-manager/pull/1442)) +- Do not throw exception when plugin file does not exist on cancelled installation ([#1442](https://github.com/scm-manager/scm-manager/pull/1442)) + ## [2.10.0] - 2020-11-20 ### Added - Delete branches directly in the UI ([#1422](https://github.com/scm-manager/scm-manager/pull/1422)) @@ -419,3 +430,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [2.6.3]: https://www.scm-manager.org/download/2.6.3 [2.7.0]: https://www.scm-manager.org/download/2.7.0 [2.7.1]: https://www.scm-manager.org/download/2.7.1 +[2.8.0]: https://www.scm-manager.org/download/2.8.0 +[2.9.0]: https://www.scm-manager.org/download/2.9.0 +[2.9.1]: https://www.scm-manager.org/download/2.9.1 +[2.10.0]: https://www.scm-manager.org/download/2.10.0 +[2.10.1]: https://www.scm-manager.org/download/2.10.1 diff --git a/docs/de/user/profile/assets/api-key-overview.png b/docs/de/user/profile/assets/api-key-overview.png index d150b8fb1b..d1fbe8f606 100644 Binary files a/docs/de/user/profile/assets/api-key-overview.png and b/docs/de/user/profile/assets/api-key-overview.png differ diff --git a/docs/de/user/profile/index.md b/docs/de/user/profile/index.md index 6e399bfa13..29018292d1 100644 --- a/docs/de/user/profile/index.md +++ b/docs/de/user/profile/index.md @@ -13,7 +13,7 @@ eingegeben werden. Danach muss das neue Passwort zweimal eingegeben werden. ## Öffentliche Schlüssel -Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen Schlüssel hinterlegt werden. +Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen GPG Schlüssel hinterlegt werden. Zudem können hier die vom SCM-Manager erstellten Signaturschlüssel heruntergeladen werden. ## API Schlüssel diff --git a/docs/de/user/repo/assets/repository-branch-detailView.png b/docs/de/user/repo/assets/repository-branch-detailView.png index d671846fef..f9727d9d4f 100644 Binary files a/docs/de/user/repo/assets/repository-branch-detailView.png and b/docs/de/user/repo/assets/repository-branch-detailView.png differ diff --git a/docs/de/user/repo/assets/repository-branches-overview.png b/docs/de/user/repo/assets/repository-branches-overview.png index 39dcf5e424..c839e6b57c 100644 Binary files a/docs/de/user/repo/assets/repository-branches-overview.png and b/docs/de/user/repo/assets/repository-branches-overview.png differ diff --git a/docs/de/user/repo/assets/repository-overview.png b/docs/de/user/repo/assets/repository-overview.png index 59cf55e4d2..2454f625a3 100644 Binary files a/docs/de/user/repo/assets/repository-overview.png and b/docs/de/user/repo/assets/repository-overview.png differ diff --git a/docs/de/user/repo/branches.md b/docs/de/user/repo/branches.md index 17afe92924..47ac022067 100644 --- a/docs/de/user/repo/branches.md +++ b/docs/de/user/repo/branches.md @@ -3,9 +3,11 @@ title: Repository subtitle: Branches --- ### Übersicht -Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet. +Auf der Branches-Übersicht sind die bereits existierenden Branches aufgeführt. Bei einem Klick auf einen Branch wird man zur Detailseite des Branches weitergeleitet. +Die Branches sind in zwei Listen aufgeteilt: Unter "Aktive Branches" sind Branches aufgelistet, deren letzter Commit +nicht 30 Tage älter als der Stand des Default-Branches ist. Alle älteren Branches sind in der Liste "Stale Branches" zu finden. -Der Tag "Default" gibt an welcher Branch aktuell, als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet. +Der Tag "Default" gibt an, welcher Branch aktuell als Standard-Branch dieses Repository im SCM-Manager markiert ist. Der Standard-Branch wird immer zuerst angezeigt, wenn man das Repository im SCM-Manager öffnet. Alle Branches mit Ausnahme des Default Branches können über den Mülleimer-Icon unwiderruflich gelöscht werden. Über den "Branch erstellen"-Button gelangt man zum Formular, um neue Branches anzulegen. diff --git a/docs/de/user/user/assets/user-information.png b/docs/de/user/user/assets/user-information.png index f5573c10e8..b79a4076c7 100644 Binary files a/docs/de/user/user/assets/user-information.png and b/docs/de/user/user/assets/user-information.png differ diff --git a/docs/de/user/user/assets/user-settings-general.png b/docs/de/user/user/assets/user-settings-general.png index 9fe75a3a89..9f63014b71 100644 Binary files a/docs/de/user/user/assets/user-settings-general.png and b/docs/de/user/user/assets/user-settings-general.png differ diff --git a/docs/de/user/user/assets/user-settings-publickeys.png b/docs/de/user/user/assets/user-settings-publickeys.png index 9761a3b044..b40d52e2a8 100644 Binary files a/docs/de/user/user/assets/user-settings-publickeys.png and b/docs/de/user/user/assets/user-settings-publickeys.png differ diff --git a/docs/en/user/profile/assets/api-key-overview.png b/docs/en/user/profile/assets/api-key-overview.png index d150b8fb1b..d052188103 100644 Binary files a/docs/en/user/profile/assets/api-key-overview.png and b/docs/en/user/profile/assets/api-key-overview.png differ diff --git a/docs/en/user/profile/index.md b/docs/en/user/profile/index.md index 50746c2f56..5895b89700 100644 --- a/docs/en/user/profile/index.md +++ b/docs/en/user/profile/index.md @@ -11,9 +11,9 @@ Here the password for the current account can be changed when it is a local acco external system). To authorize the change, the current password has to be put first. Then the new password has to be entered twice. -## Öffentliche Schlüssel +## Public Keys -To check signatures for example for commits, public keys can be stored here. Additionally the keys created by +To check signatures (for example for commits), gpg public keys can be stored here. Additionally the keys created by SCM-Manager can be accessed here, too. ## API keys diff --git a/docs/en/user/repo/assets/repository-branch-detailView.png b/docs/en/user/repo/assets/repository-branch-detailView.png index 615ba696c1..28f768a6be 100644 Binary files a/docs/en/user/repo/assets/repository-branch-detailView.png and b/docs/en/user/repo/assets/repository-branch-detailView.png differ diff --git a/docs/en/user/repo/assets/repository-branches-overview.png b/docs/en/user/repo/assets/repository-branches-overview.png index e63ebab775..08a2ab19df 100644 Binary files a/docs/en/user/repo/assets/repository-branches-overview.png and b/docs/en/user/repo/assets/repository-branches-overview.png differ diff --git a/docs/en/user/repo/assets/repository-overview.png b/docs/en/user/repo/assets/repository-overview.png index 59cf55e4d2..7e6bfcbab7 100644 Binary files a/docs/en/user/repo/assets/repository-overview.png and b/docs/en/user/repo/assets/repository-overview.png differ diff --git a/docs/en/user/repo/branches.md b/docs/en/user/repo/branches.md index 370165710b..7a9d318786 100644 --- a/docs/en/user/repo/branches.md +++ b/docs/en/user/repo/branches.md @@ -4,6 +4,8 @@ subtitle: Branches --- ### Overview The branches overview shows the branches that are already existing. By clicking on a branch, the details page of the branch is shown. +Branches are split into two lists: Branches whose last commits are at most 30 days older than the head of the default +branch are listed in "Active Branches". The older ones can be found in "Stale Branches". The tag "Default" shows which branch is currently set as the default branch of the repository in SCM-Manager. The default branch is always shown first when opening the repository in SCM-Manager. All branches except the default branch of the repository can be deleted by clicking on the trash bin icon. diff --git a/docs/en/user/user/assets/user-information.png b/docs/en/user/user/assets/user-information.png index f5573c10e8..666f05f7f8 100644 Binary files a/docs/en/user/user/assets/user-information.png and b/docs/en/user/user/assets/user-information.png differ diff --git a/docs/en/user/user/assets/user-settings-general.png b/docs/en/user/user/assets/user-settings-general.png index 4312fa0e8e..10d3688e47 100644 Binary files a/docs/en/user/user/assets/user-settings-general.png and b/docs/en/user/user/assets/user-settings-general.png differ diff --git a/docs/en/user/user/assets/user-settings-publickeys.png b/docs/en/user/user/assets/user-settings-publickeys.png index b2eae78e8b..286f46d8f2 100644 Binary files a/docs/en/user/user/assets/user-settings-publickeys.png and b/docs/en/user/user/assets/user-settings-publickeys.png differ diff --git a/pom.xml b/pom.xml index e585298b32..f25d88f521 100644 --- a/pom.xml +++ b/pom.xml @@ -580,7 +580,7 @@ sonia.scm.maven smp-maven-plugin - 1.3.0 + 1.4.0 @@ -903,7 +903,7 @@ - 3.5.15 + 3.6.0 2.1 5.7.0 @@ -937,7 +937,7 @@ 1.10.1-scm2 - 26.0-jre + 30.0-jre 12.16.1 diff --git a/scm-core/src/main/java/sonia/scm/repository/Branch.java b/scm-core/src/main/java/sonia/scm/repository/Branch.java index bdcbc66a82..acfdd6a884 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branch.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branch.java @@ -24,8 +24,6 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import sonia.scm.Validateable; @@ -34,10 +32,9 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; +import java.util.Optional; import java.util.regex.Pattern; -//~--- JDK imports ------------------------------------------------------------ - /** * Represents a branch in a repository. * @@ -46,73 +43,100 @@ import java.util.regex.Pattern; */ @XmlRootElement(name = "branch") @XmlAccessorType(XmlAccessType.FIELD) -public final class Branch implements Serializable, Validateable -{ +public final class Branch implements Serializable, Validateable { private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>"; private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/."; public static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?"; public static final Pattern VALID_BRANCH_NAME_PATTERN = Pattern.compile(VALID_BRANCH_NAMES); - /** Field description */ private static final long serialVersionUID = -4602244691711222413L; - //~--- constructors --------------------------------------------------------- + private String name; + + private String revision; + + private boolean defaultBranch; + + private Long lastCommitDate; + + private boolean stale = false; /** * Constructs a new instance of branch. * This constructor should only be called from JAXB. - * */ Branch() {} /** * Constructs a new branch. * + * @param name name of the branch + * @param revision latest revision of the branch + * @param defaultBranch Whether this branch is the default branch for the repository + * + * @deprecated Use {@link Branch#Branch(String, String, boolean, Long)} instead. + */ + @Deprecated + Branch(String name, String revision, boolean defaultBranch) { + this(name, revision, defaultBranch, null); + } + + /** + * Constructs a new branch. * * @param name name of the branch * @param revision latest revision of the branch + * @param defaultBranch Whether this branch is the default branch for the repository + * @param lastCommitDate The date of the commit this branch points to (if computed). May be null */ - Branch(String name, String revision, boolean defaultBranch) - { + Branch(String name, String revision, boolean defaultBranch, Long lastCommitDate) { this.name = name; this.revision = revision; this.defaultBranch = defaultBranch; + this.lastCommitDate = lastCommitDate; } + /** + * @deprecated Use {@link #normalBranch(String, String, Long)} instead to set the date of the last commit, too. + */ + @Deprecated public static Branch normalBranch(String name, String revision) { - return new Branch(name, revision, false); + return normalBranch(name, revision, null); } + public static Branch normalBranch(String name, String revision, Long lastCommitDate) { + return new Branch(name, revision, false, lastCommitDate); + } + + /** + * @deprecated Use {@link #defaultBranch(String, String, Long)} instead to set the date of the last commit, too. + */ + @Deprecated public static Branch defaultBranch(String name, String revision) { - return new Branch(name, revision, true); + return defaultBranch(name, revision, null); } - //~--- methods -------------------------------------------------------------- + public static Branch defaultBranch(String name, String revision, Long lastCommitDate) { + return new Branch(name, revision, true, lastCommitDate); + } + + public void setStale(boolean stale) { + this.stale = stale; + } @Override public boolean isValid() { return VALID_BRANCH_NAME_PATTERN.matcher(name).matches(); } - /** - * {@inheritDoc} - * - * - * @param obj - * - * @return - */ @Override - public boolean equals(Object obj) - { - if (obj == null) - { + public boolean equals(Object obj) { + if (obj == null) { return false; } - if (getClass() != obj.getClass()) - { + if (getClass() != obj.getClass()) { return false; } @@ -120,48 +144,31 @@ public final class Branch implements Serializable, Validateable return Objects.equal(name, other.name) && Objects.equal(revision, other.revision) - && Objects.equal(defaultBranch, other.defaultBranch); + && Objects.equal(defaultBranch, other.defaultBranch) + && Objects.equal(lastCommitDate, other.lastCommitDate); } - /** - * {@inheritDoc} - * - * - * @return - */ @Override - public int hashCode() - { + public int hashCode() { return Objects.hashCode(name, revision); } - /** - * {@inheritDoc} - * - * - * @return - */ @Override - public String toString() - { - //J- + public String toString() { return MoreObjects.toStringHelper(this) .add("name", name) .add("revision", revision) + .add("defaultBranch", defaultBranch) + .add("lastCommitDate", lastCommitDate) .toString(); - //J+ } - //~--- get methods ---------------------------------------------------------- - /** * Returns the name of the branch * - * * @return name of the branch */ - public String getName() - { + public String getName() { return name; } @@ -170,22 +177,27 @@ public final class Branch implements Serializable, Validateable * * @return latest revision of branch */ - public String getRevision() - { + public String getRevision() { return revision; } + /** + * Flag whether this branch is configured as the default branch. + */ public boolean isDefaultBranch() { return defaultBranch; } - //~--- fields --------------------------------------------------------------- + /** + * The date of the commit this branch points to, if this was computed (can be empty). + * + * @since 2.11.0 + */ + public Optional getLastCommitDate() { + return Optional.ofNullable(lastCommitDate); + } - /** name of the branch */ - private String name; - - /** Field description */ - private String revision; - - private boolean defaultBranch; + public boolean isStale() { + return stale; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputer.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputer.java new file mode 100644 index 0000000000..15de6663ec --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputer.java @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and 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. + */ + +package sonia.scm.repository.api; + +import sonia.scm.repository.Branch; +import sonia.scm.repository.spi.BranchStaleComputer; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static java.time.Instant.ofEpochMilli; + +public class BranchXDaysOlderThanDefaultStaleComputer implements BranchStaleComputer { + + public static final int DEFAULT_AMOUNT_OF_DAYS = 30; + + private final int amountOfDays; + + public BranchXDaysOlderThanDefaultStaleComputer() { + this(DEFAULT_AMOUNT_OF_DAYS); + } + + public BranchXDaysOlderThanDefaultStaleComputer(int amountOfDays) { + this.amountOfDays = amountOfDays; + } + + @Override + @SuppressWarnings("java:S3655") // we check "isPresent" for both dates, but due to the third check sonar does not get it + public boolean computeStale(Branch branch, StaleContext context) { + Branch defaultBranch = context.getDefaultBranch(); + if (shouldCompute(branch, defaultBranch)) { + Instant defaultCommitDate = ofEpochMilli(defaultBranch.getLastCommitDate().get()); + Instant thisCommitDate = ofEpochMilli(branch.getLastCommitDate().get()); + return thisCommitDate.plus(amountOfDays, ChronoUnit.DAYS).isBefore(defaultCommitDate); + } else { + return false; + } + } + + public boolean shouldCompute(Branch branch, Branch defaultBranch) { + return !branch.isDefaultBranch() && branch.getLastCommitDate().isPresent() && defaultBranch.getLastCommitDate().isPresent(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java index 5ba45b00a0..af784f287b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BranchesCommandBuilder.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.api; import com.google.common.base.Objects; @@ -165,7 +165,7 @@ public final class BranchesCommandBuilder private Branches getBranchesFromCommand() throws IOException { - return new Branches(branchesCommand.getBranches()); + return new Branches(branchesCommand.getBranchesWithStaleFlags(new BranchXDaysOlderThanDefaultStaleComputer())); } //~--- inner classes -------------------------------------------------------- diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchStaleComputer.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchStaleComputer.java new file mode 100644 index 0000000000..c6324fa1cf --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchStaleComputer.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and 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. + */ + +package sonia.scm.repository.spi; + +import lombok.Data; +import sonia.scm.repository.Branch; + +public interface BranchStaleComputer { + + boolean computeStale(Branch branch, StaleContext context); + + @Data + class StaleContext { + private Branch defaultBranch; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java b/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java index ae1468e768..9fa28a49aa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/BranchesCommand.java @@ -21,33 +21,52 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.repository.spi; import sonia.scm.repository.Branch; import java.io.IOException; import java.util.List; - -//~--- JDK imports ------------------------------------------------------------ +import java.util.Optional; /** - * * @author Sebastian Sdorra * @since 1.18 */ -public interface BranchesCommand -{ +public interface BranchesCommand { - /** - * Method description - * - * - * @return - * - * @throws IOException - */ List getBranches() throws IOException; + + default List getBranchesWithStaleFlags(BranchStaleComputer computer) throws IOException { + List branches = getBranches(); + new StaleProcessor(computer, branches).process(); + return branches; + } + + final class StaleProcessor { + + private final BranchStaleComputer computer; + private final List branches; + + private StaleProcessor(BranchStaleComputer computer, List branches) { + this.computer = computer; + this.branches = branches; + } + + private void process() { + Optional defaultBranch = branches.stream() + .filter(Branch::isDefaultBranch) + .findFirst(); + + defaultBranch.ifPresent(this::process); + } + + private void process(Branch defaultBranch) { + BranchStaleComputer.StaleContext staleContext = new BranchStaleComputer.StaleContext(); + staleContext.setDefaultBranch(defaultBranch); + + branches.forEach(branch -> branch.setStale(computer.computeStale(branch, staleContext))); + } + } } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputerTest.java b/scm-core/src/test/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputerTest.java new file mode 100644 index 0000000000..12fb269162 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/BranchXDaysOlderThanDefaultStaleComputerTest.java @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and 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. + */ + +package sonia.scm.repository.api; + +import org.junit.jupiter.api.Test; +import sonia.scm.repository.Branch; +import sonia.scm.repository.spi.BranchStaleComputer; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static java.time.Instant.now; +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.Branch.defaultBranch; +import static sonia.scm.repository.Branch.normalBranch; + +class BranchXDaysOlderThanDefaultStaleComputerTest { + + Instant now = now(); + + BranchXDaysOlderThanDefaultStaleComputer computer = new BranchXDaysOlderThanDefaultStaleComputer(30); + + @Test + void shouldTagOldBranchAsStale() { + long staleTime = + now + .minus(30, ChronoUnit.DAYS) + .minus(1, ChronoUnit.MINUTES) + .toEpochMilli(); + + Branch branch = normalBranch("hog", "42", staleTime); + boolean stale = computer.computeStale(branch, createStaleContext()); + + assertThat(stale).isTrue(); + } + + @Test + void shouldNotTagNotSoOldBranchAsStale() { + long activeTime = + now + .minus(30, ChronoUnit.DAYS) + .plus(1, ChronoUnit.MINUTES) + .toEpochMilli(); + + Branch branch = normalBranch("hog", "42", activeTime); + boolean stale = computer.computeStale(branch, createStaleContext()); + + assertThat(stale).isFalse(); + } + + @Test + void shouldNotTagDefaultBranchAsStale() { + long staleTime = + now + .minus(30, ChronoUnit.DAYS) + .minus(1, ChronoUnit.MINUTES) + .toEpochMilli(); + + Branch branch = defaultBranch("hog", "42", staleTime); + boolean stale = computer.computeStale(branch, createStaleContext()); + + assertThat(stale).isFalse(); + } + + BranchStaleComputer.StaleContext createStaleContext() { + BranchStaleComputer.StaleContext staleContext = new BranchStaleComputer.StaleContext(); + staleContext.setDefaultBranch(defaultBranch("default", "23", now.toEpochMilli())); + return staleContext; + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/BranchesCommandTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/BranchesCommandTest.java new file mode 100644 index 0000000000..4a0f9326c0 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/BranchesCommandTest.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and 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. + */ + +package sonia.scm.repository.spi; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import sonia.scm.repository.Branch; +import sonia.scm.repository.api.BranchXDaysOlderThanDefaultStaleComputer; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static java.time.Instant.now; +import static java.util.Arrays.asList; + +class BranchesCommandTest { + + @Test + void shouldMarkEachBranchDependingOnDefaultBranch() throws IOException { + Instant now = now(); + long staleTime = + now + .minus(30, ChronoUnit.DAYS) + .minus(1, ChronoUnit.MINUTES) + .toEpochMilli(); + long activeTime = + now + .minus(30, ChronoUnit.DAYS) + .plus(1, ChronoUnit.MINUTES) + .toEpochMilli(); + + List branches = asList( + Branch.normalBranch("arthur", "42", staleTime), + Branch.normalBranch("marvin", "42", staleTime), + Branch.defaultBranch("hog", "42", now.toEpochMilli()), + Branch.normalBranch("trillian", "42", activeTime) + ); + + List branchesWithStaleFlags = new BranchesCommand() { + @Override + public List getBranches() { + return branches; + } + }.getBranchesWithStaleFlags(new BranchXDaysOlderThanDefaultStaleComputer()); + + Assertions.assertThat(branchesWithStaleFlags) + .extracting("stale") + .containsExactly(true, true, false, false); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java index d026affd8b..683e5d0068 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitBranchesCommand.java @@ -24,14 +24,14 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import org.checkerframework.checker.nullness.qual.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.repository.Branch; @@ -44,12 +44,9 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -//~--- JDK imports ------------------------------------------------------------ +import static sonia.scm.repository.GitUtil.getCommit; +import static sonia.scm.repository.GitUtil.getCommitTime; -/** - * - * @author Sebastian Sdorra - */ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCommand { private static final Logger LOG = LoggerFactory.getLogger(GitBranchesCommand.class); @@ -60,23 +57,22 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo super(context); } - //~--- get methods ---------------------------------------------------------- - @Override public List getBranches() throws IOException { Git git = createGit(); String defaultBranchName = determineDefaultBranchName(git); - try { + Repository repository = git.getRepository(); + try (RevWalk refWalk = new RevWalk(repository)) { return git .branchList() .call() .stream() - .map(ref -> createBranchObject(defaultBranchName, ref)) + .map(ref -> createBranchObject(repository, refWalk, defaultBranchName, ref)) .collect(Collectors.toList()); } catch (GitAPIException ex) { - throw new InternalRepositoryException(repository, "could not read branches", ex); + throw new InternalRepositoryException(this.repository, "could not read branches", ex); } } @@ -86,21 +82,31 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo } @Nullable - private Branch createBranchObject(String defaultBranchName, Ref ref) { + private Branch createBranchObject(Repository repository, RevWalk refWalk, String defaultBranchName, Ref ref) { String branchName = GitUtil.getBranch(ref); if (branchName == null) { LOG.warn("could not determine branch name for branch name {} at revision {}", ref.getName(), ref.getObjectId()); return null; } else { + Long lastCommitDate = getCommitDate(repository, refWalk, branchName, ref); if (branchName.equals(defaultBranchName)) { - return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId())); + return Branch.defaultBranch(branchName, GitUtil.getId(ref.getObjectId()), lastCommitDate); } else { - return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId())); + return Branch.normalBranch(branchName, GitUtil.getId(ref.getObjectId()), lastCommitDate); } } } + private Long getCommitDate(Repository repository, RevWalk refWalk, String branchName, Ref ref) { + try { + return getCommitTime(getCommit(repository, refWalk, ref)); + } catch (IOException e) { + LOG.info("failed to read commit date of branch {} with revision {}", branchName, ref.getName()); + return null; + } + } + private String determineDefaultBranchName(Git git) { String defaultBranchName = context.getConfig().getDefaultBranch(); if (Strings.isNullOrEmpty(defaultBranchName)) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java index 967226012a..fddf386d92 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitBranchesCommandTest.java @@ -24,117 +24,26 @@ package sonia.scm.repository.spi; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.ListBranchCommand; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.Test; import sonia.scm.repository.Branch; -import sonia.scm.repository.GitRepositoryConfig; import java.io.IOException; import java.util.List; -import java.util.Optional; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Optional.of; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) -class GitBranchesCommandTest { - - @Mock - GitContext context; - @Mock - Git git; - @Mock - ListBranchCommand listBranchCommand; - @Mock - GitRepositoryConfig gitRepositoryConfig; - - GitBranchesCommand branchesCommand; - private Ref master; - - @BeforeEach - void initContext() { - when(context.getConfig()).thenReturn(gitRepositoryConfig); - } - - @BeforeEach - void initCommand() { - master = createRef("master", "0000"); - branchesCommand = new GitBranchesCommand(context) { - @Override - Git createGit() { - return git; - } - - @Override - Optional getRepositoryHeadRef(Git git) { - return of(master); - } - }; - when(git.branchList()).thenReturn(listBranchCommand); - } +public class GitBranchesCommandTest extends AbstractGitCommandTestBase { @Test - void shouldCreateEmptyListWithoutBranches() throws IOException, GitAPIException { - when(listBranchCommand.call()).thenReturn(emptyList()); + public void shouldReadBranches() throws IOException { + GitBranchesCommand branchesCommand = new GitBranchesCommand(createContext()); List branches = branchesCommand.getBranches(); - assertThat(branches).isEmpty(); - } - - @Test - void shouldMapNormalBranch() throws IOException, GitAPIException { - Ref branch = createRef("branch", "1337"); - when(listBranchCommand.call()).thenReturn(asList(branch)); - - List branches = branchesCommand.getBranches(); - - assertThat(branches).containsExactly(Branch.normalBranch("branch", "1337")); - } - - @Test - void shouldMarkMasterBranchWithMasterFromConfig() throws IOException, GitAPIException { - Ref branch = createRef("branch", "1337"); - when(listBranchCommand.call()).thenReturn(asList(branch)); - when(gitRepositoryConfig.getDefaultBranch()).thenReturn("branch"); - - List branches = branchesCommand.getBranches(); - - assertThat(branches).containsExactlyInAnyOrder(Branch.defaultBranch("branch", "1337")); - } - - @Test - void shouldMarkMasterBranchWithMasterFromHead() throws IOException, GitAPIException { - Ref branch = createRef("branch", "1337"); - when(listBranchCommand.call()).thenReturn(asList(branch, master)); - - List branches = branchesCommand.getBranches(); - - assertThat(branches).containsExactlyInAnyOrder( - Branch.normalBranch("branch", "1337"), - Branch.defaultBranch("master", "0000") + assertThat(branches).contains( + Branch.defaultBranch("master", "fcd0ef1831e4002ac43ea539f4094334c79ea9ec", 1339428655000L), + Branch.normalBranch("mergeable", "91b99de908fcd04772798a31c308a64aea1a5523", 1541586052000L), + Branch.normalBranch("rename", "383b954b27e052db6880d57f1c860dc208795247", 1589203061000L) ); } - - private Ref createRef(String branchName, String revision) { - Ref ref = mock(Ref.class); - lenient().when(ref.getName()).thenReturn("refs/heads/" + branchName); - ObjectId objectId = mock(ObjectId.class); - lenient().when(objectId.name()).thenReturn(revision); - lenient().when(ref.getObjectId()).thenReturn(objectId); - return ref; - } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java index 22cccbc8ae..a2285bad11 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgBranchesCommand.java @@ -27,7 +27,6 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.aragost.javahg.Changeset; -import com.google.common.base.Function; import com.google.common.collect.Lists; import sonia.scm.repository.Branch; @@ -63,14 +62,8 @@ public class HgBranchesCommand extends AbstractCommand List hgBranches = com.aragost.javahg.commands.BranchesCommand.on(open()).execute(); - List branches = Lists.transform(hgBranches, - new Function() - { - - @Override - public Branch apply(com.aragost.javahg.Branch hgBranch) - { + return Lists.transform(hgBranches, + hgBranch -> { String node = null; Changeset changeset = hgBranch.getBranchTip(); @@ -79,14 +72,12 @@ public class HgBranchesCommand extends AbstractCommand node = changeset.getNode(); } + long lastCommitDate = changeset.getTimestamp().getDate().getTime(); if (DEFAULT_BRANCH_NAME.equals(hgBranch.getName())) { - return Branch.defaultBranch(hgBranch.getName(), node); + return Branch.defaultBranch(hgBranch.getName(), node, lastCommitDate); } else { - return Branch.normalBranch(hgBranch.getName(), node); + return Branch.normalBranch(hgBranch.getName(), node, lastCommitDate); } - } - }); - - return branches; + }); } } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchesCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchesCommandTest.java new file mode 100644 index 0000000000..a8f43eb435 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgBranchesCommandTest.java @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and 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. + */ + +package sonia.scm.repository.spi; + +import org.junit.Test; +import sonia.scm.repository.Branch; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.Branch.defaultBranch; +import static sonia.scm.repository.Branch.normalBranch; + +public class HgBranchesCommandTest extends AbstractHgCommandTestBase { + + @Test + public void shouldReadBranches() { + HgBranchesCommand command = new HgBranchesCommand(cmdContext); + + List branches = command.getBranches(); + + assertThat(branches).contains( + defaultBranch("default", "2baab8e80280ef05a9aa76c49c76feca2872afb7", 1339586381000L), + normalBranch("test-branch", "79b6baf49711ae675568e0698d730b97ef13e84a", 1339586299000L) + ); + } +} diff --git a/scm-ui/ui-components/src/BranchSelector.stories.tsx b/scm-ui/ui-components/src/BranchSelector.stories.tsx index 5abc1a2202..8f59b379f0 100644 --- a/scm-ui/ui-components/src/BranchSelector.stories.tsx +++ b/scm-ui/ui-components/src/BranchSelector.stories.tsx @@ -24,14 +24,14 @@ import { storiesOf } from "@storybook/react"; import { BranchSelector } from "./index"; -import { Branch } from "@scm-manager/ui-types/src"; +import { Branch } from "@scm-manager/ui-types"; import * as React from "react"; import styled from "styled-components"; const master = { name: "master", revision: "1", defaultBranch: true, _links: {} }; const develop = { name: "develop", revision: "2", defaultBranch: false, _links: {} }; -const branchSelected = (branch?: Branch) => {}; +const branchSelected = (branch?: Branch) => null; const branches = [master, develop]; @@ -42,6 +42,4 @@ const Wrapper = styled.div` storiesOf("BranchSelector", module) .addDecorator(storyFn => {storyFn()}) - .add("Default", () => ( - -)); + .add("Default", () => ); diff --git a/scm-ui/ui-components/src/OverviewPageActions.tsx b/scm-ui/ui-components/src/OverviewPageActions.tsx index 4cbffb32ef..633bc07d52 100644 --- a/scm-ui/ui-components/src/OverviewPageActions.tsx +++ b/scm-ui/ui-components/src/OverviewPageActions.tsx @@ -21,13 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React from "react"; -import { withRouter, RouteComponentProps } from "react-router-dom"; +import React, { FC } from "react"; +import { useHistory, useLocation } from "react-router-dom"; import classNames from "classnames"; import { Button, DropDown, urls } from "./index"; import { FilterInput } from "./forms"; -type Props = RouteComponentProps & { +type Props = { showCreateButton: boolean; currentGroup: string; groups: string[]; @@ -35,41 +35,33 @@ type Props = RouteComponentProps & { groupSelected: (namespace: string) => void; label?: string; testId?: string; + searchPlaceholder?: string; }; -class OverviewPageActions extends React.Component { - render() { - const { history, currentGroup, groups, location, link, testId, groupSelected } = this.props; - const groupSelector = groups && ( -
- -
- ); +const OverviewPageActions: FC = ({ + groups, + currentGroup, + showCreateButton, + link, + groupSelected, + label, + testId, + searchPlaceholder +}) => { + const history = useHistory(); + const location = useLocation(); + const groupSelector = groups && ( +
+ +
+ ); - return ( -
- {groupSelector} -
- { - history.push(`/${link}/?q=${filter}`); - }} - testId={testId + "-filter"} - /> -
- {this.renderCreateButton()} -
- ); - } - - renderCreateButton() { - const { showCreateButton, link, label } = this.props; + const renderCreateButton = () => { if (showCreateButton) { return (
@@ -78,7 +70,24 @@ class OverviewPageActions extends React.Component { ); } return null; - } -} + }; -export default withRouter(OverviewPageActions); + return ( +
+ {groupSelector} +
+ { + history.push(`/${link}/?q=${filter}`); + }} + testId={testId + "-filter"} + /> +
+ {renderCreateButton()} +
+ ); +}; + +export default OverviewPageActions; diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index f2fd0d93d8..eb97f1ef92 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -50439,36 +50439,70 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = ` href="/repo/hitchhiker/heartOfGold/branches/" onClick={[Function]} > - + + + + + + + + - + + + - + + + - + + +
{ }; renderBranchesLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["branches"]) { - return ; + return ( + + ); + } + return null; + }; + + renderTagsLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; + if (repository._links["tags"]) { + return ( + + ); } return null; }; renderChangesetsLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["changesets"]) { - return ; + return ( + + ); } return null; }; renderSourcesLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["sources"]) { - return ; + return ( + + ); } return null; }; renderModifyLink = (repository: Repository, repositoryLink: string) => { + const { t } = this.props; if (repository._links["update"]) { - return ; + return ( + + ); } return null; }; @@ -74,6 +113,7 @@ class RepositoryEntry extends React.Component { return ( <> {this.renderBranchesLink(repository, repositoryLink)} + {this.renderTagsLink(repository, repositoryLink)} {this.renderChangesetsLink(repository, repositoryLink)} {this.renderSourcesLink(repository, repositoryLink)} @@ -118,4 +158,4 @@ class RepositoryEntry extends React.Component { } } -export default RepositoryEntry; +export default withTranslation("repos")(RepositoryEntry); diff --git a/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx b/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx index f52859c15e..a0adaba7a1 100644 --- a/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryEntryLink.tsx @@ -25,10 +25,12 @@ import React from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; import { Icon } from "@scm-manager/ui-components"; +import Tooltip from "../Tooltip"; type Props = { to: string; icon: string; + tooltip?: string; }; const PointerEventsLink = styled(Link)` @@ -37,10 +39,20 @@ const PointerEventsLink = styled(Link)` class RepositoryEntryLink extends React.Component { render() { - const { to, icon } = this.props; + const { to, icon, tooltip } = this.props; + + let content = ; + if (tooltip) { + content = ( + + {content} + + ); + } + return ( - + {content} ); } diff --git a/scm-ui/ui-types/src/Branches.ts b/scm-ui/ui-types/src/Branches.ts index 87eb5af3dc..949f7b40bc 100644 --- a/scm-ui/ui-types/src/Branches.ts +++ b/scm-ui/ui-types/src/Branches.ts @@ -28,6 +28,8 @@ export type Branch = { name: string; revision: string; defaultBranch?: boolean; + lastCommitDate?: string; + stale?: boolean; _links: Links; }; diff --git a/scm-ui/ui-webapp/public/locales/de/groups.json b/scm-ui/ui-webapp/public/locales/de/groups.json index 65035781b6..a351a3eda1 100644 --- a/scm-ui/ui-webapp/public/locales/de/groups.json +++ b/scm-ui/ui-webapp/public/locales/de/groups.json @@ -25,6 +25,9 @@ "setPermissionsNavLink": "Berechtigungen" } }, + "overview": { + "searchGroup": "Gruppe suchen" + }, "add-group": { "title": "Gruppe erstellen", "subtitle": "Erstellen einer neuen Gruppe" diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index d95ef44970..6433fb348a 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -36,13 +36,22 @@ "settingsNavLink": "Einstellungen", "generalNavLink": "Generell", "permissionsNavLink": "Berechtigungen" + }, + "tooltip": { + "branches": "Branches", + "tags": "Tags", + "commits": "Commits", + "sources": "Sources", + "settings": "Einstellungen" } }, "overview": { "title": "Repositories", "subtitle": "Übersicht aller verfügbaren Repositories", "noRepositories": "Keine Repositories gefunden.", - "createButton": "Repository erstellen" + "createButton": "Repository erstellen", + "searchRepository": "Repository suchen", + "allNamespaces": "Alle Namespaces" }, "create": { "title": "Repository erstellen", @@ -55,7 +64,11 @@ "createButton": "Branch erstellen" }, "table": { - "branches": "Branches" + "branches": { + "active": "Aktive Branches", + "stale": "Alte Branches" + }, + "lastCommit": "Letzter Commit" }, "create": { "title": "Branch erstellen", diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json index 80d251e6b3..cb2a599aac 100644 --- a/scm-ui/ui-webapp/public/locales/de/users.json +++ b/scm-ui/ui-webapp/public/locales/de/users.json @@ -30,6 +30,9 @@ "noUsers": "Keine Benutzer gefunden.", "createButton": "Benutzer erstellen" }, + "overview": { + "searchUser": "Benutzer suchen" + }, "singleUser": { "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Benutzer Fehler", @@ -76,15 +79,22 @@ } }, "publicKey": { + "subtitle": "Öffentliche Schlüssel", + "description": "Zum Prüfen von Signaturen für z. B. Commits können hier die entsprechenden öffentlichen GPG Schlüssel hinterlegt werden. Zudem können hier die vom SCM-Manager erstellten Signaturschlüssel heruntergeladen werden.", "noStoredKeys": "Es wurden keine Schlüssel gefunden.", "displayName": "Anzeigename", - "raw": "Schlüssel", "created": "Eingetragen an", - "addKey": "Schlüssel hinzufügen", + "raw": "Schlüssel", + "download": "Herunterladen", "delete": "Löschen", - "download": "Herunterladen" + "addSubtitle": "Neuen Schlüssel hinzufügen", + "addKey": "Schlüssel hinzufügen" }, "apiKey": { + "subtitle": "API Schlüssel", + "text1": "Erstelle und verwalte Personal Access Token um auf die REST API zuzugreifen oder diese als Passwort für SCM-Clients zu nutzen. Die Rechte der Token sind auf Repositories und die gewählte Rolle beschränkt.", + "manageRoles": "Sie können die Rollenberechtigungen in der Administration unter „Berechtigungsrollen“ einsehen und neue Rollen anlegen.", + "text2": "Um den Token in REST-Abfragen zu nutzen, übergeben Sie diesen als Cookie mit dem Namen „X-Bearer-Token“. Sie können den Token auch anstelle Ihres Passworts nutzen, um sich mit SCM-Clients anzumelden.", "noStoredKeys": "Es wurden keine Schlüssel gefunden.", "displayName": "Anzeigename", "permissionRole": { @@ -92,12 +102,10 @@ "help": "Mit der Rolle können Sie die Berechtigung für diesen Schlüssel einschränken" }, "created": "Eingetragen an", + "addSubtitle": "Neuen Schlüssel hinzufügen", "addKey": "Schlüssel hinzufügen", "delete": "Löschen", "download": "Herunterladen", - "text1": "Erstelle und verwalte Personal Access Token um auf die REST API zuzugreifen oder diese als Passwort für SCM-Clients zu nutzen. Die Rechte der Token sind auf Repositories und die gewählte Rolle beschränkt.", - "manageRoles": "Sie können die Rollenberechtigungen in der Administration unter „Berechtigungsrollen“ einsehen und neue Rollen anlegen.", - "text2": "Um den Token in REST-Abfragen zu nutzen, übergeben Sie diesen als Cookie mit dem Namen „X-Bearer-Token“. Sie können den Token auch anstelle Ihres Passworts nutzen, um sich mit SCM-Clients anzumelden.", "modal": { "title": "Schlüssel erzeugt", "text1": "Ihr neuer API-Schlüssel ist bereit. Sie können diesen als Token für Zugriffe auf die REST-Schnittstelle nutzen oder anstelle Ihres Passworts zum Login mit SCM-Clients nutzen.", diff --git a/scm-ui/ui-webapp/public/locales/en/groups.json b/scm-ui/ui-webapp/public/locales/en/groups.json index 069013e5d0..944d271071 100644 --- a/scm-ui/ui-webapp/public/locales/en/groups.json +++ b/scm-ui/ui-webapp/public/locales/en/groups.json @@ -25,6 +25,9 @@ "setPermissionsNavLink": "Permissions" } }, + "overview": { + "searchGroup": "Search group" + }, "add-group": { "title": "Create Group", "subtitle": "Create a new group" diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 59981282df..cc4996b937 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -36,13 +36,22 @@ "settingsNavLink": "Settings", "generalNavLink": "General", "permissionsNavLink": "Permissions" + }, + "tooltip": { + "branches": "Branches", + "tags": "Tags", + "commits": "Commits", + "sources": "Sources", + "settings": "Settings" } }, "overview": { "title": "Repositories", "subtitle": "Overview of available repositories", "noRepositories": "No repositories found.", - "createButton": "Create Repository" + "createButton": "Create Repository", + "searchRepository": "Search repository", + "allNamespaces": "All namespaces" }, "create": { "title": "Create Repository", @@ -55,7 +64,11 @@ "createButton": "Create Branch" }, "table": { - "branches": "Branches" + "branches": { + "active": "Active Branches", + "stale": "Stale Branches" + }, + "lastCommit": "Last commit" }, "create": { "title": "Create Branch", diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json index 627e2b80aa..eb145f440e 100644 --- a/scm-ui/ui-webapp/public/locales/en/users.json +++ b/scm-ui/ui-webapp/public/locales/en/users.json @@ -44,6 +44,9 @@ "setApiKeyNavLink": "API Keys" } }, + "overview": { + "searchUser": "Search user" + }, "createUser": { "title": "Create User", "subtitle": "Create a new user" @@ -76,15 +79,22 @@ } }, "publicKey": { + "subtitle": "Public Keys", + "description": "To check signatures (for example for commits), gpg public keys can be stored here. Additionally the keys created by SCM-Manager can be accessed here, too.", "noStoredKeys": "No keys found.", "displayName": "Display Name", - "raw": "Key", "created": "Created on", - "addKey": "Add key", + "raw": "Key", + "download": "Download", "delete": "Delete", - "download": "Download" + "addSubtitle": "Add new key", + "addKey": "Add key" }, "apiKey": { + "subtitle": "API Keys", + "text1": "Create and manage personal access tokens to access the REST API or use as a password for SCM clients. The privileges of these tokens are limited to repositories and the selected role.", + "manageRoles": "You may view and create roles in the administration view “Permission Roles”.", + "text2": "To use the token in a REST request, pass it as a cookie named “X-Bearer-Token”. You may use the token as your password for SCM clients, too.", "noStoredKeys": "No keys found.", "displayName": "Display Name", "permissionRole": { @@ -92,12 +102,10 @@ "help": "The api key will be restricted to permissions of this role" }, "created": "Created on", + "addSubtitle": "Add new key", "addKey": "Add key", "delete": "Delete", "download": "Download", - "text1": "Create and manage personal access tokens to access the REST API or use as a password for SCM clients. The privileges of these tokens are limited to repositories and the selected role.", - "manageRoles": "You may view and create roles in the administration view “Permission Roles”.", - "text2": "To use the token in a REST request, pass it as a cookie named “X-Bearer-Token”. You may use the token as your password for SCM clients, too.", "modal": { "title": "Key created", "text1": "Your new API key is ready. You can use it as a bearer token for REST calls or as a password for SCM clients.", diff --git a/scm-ui/ui-webapp/src/groups/containers/Groups.tsx b/scm-ui/ui-webapp/src/groups/containers/Groups.tsx index 7fe45d0163..2584239ce0 100644 --- a/scm-ui/ui-webapp/src/groups/containers/Groups.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/Groups.tsx @@ -92,7 +92,12 @@ class Groups extends React.Component { {this.renderGroupTable()} {this.renderCreateButton()} - + ); diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx index 42bfd781b1..224187401f 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx @@ -26,21 +26,45 @@ import { Branch, Repository } from "@scm-manager/ui-types"; import { WithTranslation, withTranslation } from "react-i18next"; import BranchButtonGroup from "./BranchButtonGroup"; import DefaultBranchTag from "./DefaultBranchTag"; +import { DateFromNow } from "@scm-manager/ui-components"; +import styled from "styled-components"; type Props = WithTranslation & { repository: Repository; branch: Branch; }; +const FlexRow = styled.div` + display: flex; + align-items: center; + flex-wrap: wrap; +`; + +const Created = styled.div` + margin-left: 0.5rem; + font-size: 0.8rem; +`; + +const Label = styled.strong` + margin-right: 0.3rem; +`; + +const Date = styled(DateFromNow)` + font-size: 0.8rem; +`; + class BranchDetail extends React.Component { render() { const { repository, branch, t } = this.props; return (
-
- {t("branch.name")} {branch.name} -
+ + {branch.name} + + {t("tags.overview.created")} + +
diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx index 1acaa78159..b13e0d9893 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx @@ -25,8 +25,9 @@ import React, { FC } from "react"; import { Link as ReactLink } from "react-router-dom"; import { Branch, Link } from "@scm-manager/ui-types"; import DefaultBranchTag from "./DefaultBranchTag"; -import { Icon } from "@scm-manager/ui-components"; +import { DateFromNow, Icon } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; +import styled from "styled-components"; type Props = { baseUrl: string; @@ -34,6 +35,11 @@ type Props = { onDelete: (branch: Branch) => void; }; +const Created = styled.span` + margin-left: 1rem; + font-size: 0.8rem; +`; + const BranchRow: FC = ({ baseUrl, branch, onDelete }) => { const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; const [t] = useTranslation("repos"); @@ -56,6 +62,11 @@ const BranchRow: FC = ({ baseUrl, branch, onDelete }) => { {branch.name} + {branch.lastCommitDate && ( + + {t("branches.table.lastCommit")} + + )} {deleteButton} diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx index 312275b663..f78fc5a447 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchTable.tsx @@ -30,10 +30,11 @@ import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-comp type Props = { baseUrl: string; branches: Branch[]; + type: string; fetchBranches: () => void; }; -const BranchTable: FC = ({ baseUrl, branches, fetchBranches }) => { +const BranchTable: FC = ({ baseUrl, branches, type, fetchBranches }) => { const [t] = useTranslation("repos"); const [showConfirmAlert, setShowConfirmAlert] = useState(false); const [error, setError] = useState(); @@ -92,7 +93,7 @@ const BranchTable: FC = ({ baseUrl, branches, fetchBranches }) => { - + {renderRow()} diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx index 8bbcac53bf..37ff8cf327 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx @@ -84,7 +84,28 @@ class BranchesOverview extends React.Component { const { baseUrl, branches, repository, fetchBranches, t } = this.props; if (branches && branches.length > 0) { orderBranches(branches); - return fetchBranches(repository)} />; + const staleBranches = branches.filter(b => b.stale); + const activeBranches = branches.filter(b => !b.stale); + return ( + <> + {activeBranches.length > 0 && ( + fetchBranches(repository)} + /> + )} + {staleBranches.length > 0 && ( + fetchBranches(repository)} + /> + )} + + ); } return {t("branches.overview.noBranches")}; } diff --git a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts index afdd325a90..13024ace8a 100644 --- a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts +++ b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.test.ts @@ -47,9 +47,14 @@ const developBranch = { revision: "revision5", defaultBranch: false }; +const mainBranch = { + name: "main", + revision: "revision6", + defaultBranch: false +}; const masterBranch = { name: "master", - revision: "revision6", + revision: "revision7", defaultBranch: false }; @@ -66,10 +71,10 @@ describe("order branches", () => { expect(branches).toEqual([branch3, branch1, branch2]); }); - it("should order special branches as follows: master > default > develop", () => { - const branches = [defaultBranch, developBranch, masterBranch]; + it("should order special branches as follows: main > master > default > develop", () => { + const branches = [defaultBranch, mainBranch, developBranch, masterBranch]; orderBranches(branches); - expect(branches).toEqual([masterBranch, defaultBranch, developBranch]); + expect(branches).toEqual([mainBranch, masterBranch, defaultBranch, developBranch]); }); it("should order special branches but starting with defaultBranch", () => { diff --git a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts index 69102d5d88..827d791fbe 100644 --- a/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts +++ b/scm-ui/ui-webapp/src/repos/branches/util/orderBranches.ts @@ -32,10 +32,14 @@ export function orderBranches(branches: Branch[]) { return -20; } else if (!a.defaultBranch && b.defaultBranch) { return 20; - } else if (a.name === "master" && b.name !== "master") { + } else if (a.name === "main" && b.name !== "main") { return -10; - } else if (a.name !== "master" && b.name === "master") { + } else if (a.name !== "main" && b.name === "main") { return 10; + } else if (a.name === "master" && b.name !== "master") { + return -9; + } else if (a.name !== "master" && b.name === "master") { + return 9; } else if (a.name === "default" && b.name !== "default") { return -10; } else if (a.name !== "default" && b.name === "default") { diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx index 936c5c2ebb..a2c342f51f 100644 --- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx +++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx @@ -23,21 +23,25 @@ */ import React from "react"; import { Link } from "react-router-dom"; -import { CardColumnGroup, RepositoryEntry } from "@scm-manager/ui-components"; +import { CardColumnGroup, Icon, RepositoryEntry } from "@scm-manager/ui-components"; import { RepositoryGroup } from "@scm-manager/ui-types"; -import { Icon } from "@scm-manager/ui-components"; import { WithTranslation, withTranslation } from "react-i18next"; +import styled from "styled-components"; type Props = WithTranslation & { group: RepositoryGroup; }; +const SizedIcon = styled(Icon)` + font-size: 1.33rem; +`; + class RepositoryGroupEntry extends React.Component { render() { const { group, t } = this.props; const settingsLink = group.namespace?._links?.permissions && ( - + ); const namespaceHeader = ( diff --git a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx index 0dae2f03d9..de5be31591 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx @@ -101,8 +101,12 @@ class Overview extends React.Component { } }; + getNamespaceFilterPlaceholder = () => { + return this.props.t("overview.allNamespaces"); + }; + namespaceSelected = (newNamespace: string) => { - if (newNamespace === "") { + if (newNamespace === this.getNamespaceFilterPlaceholder()) { this.props.history.push("/repos/"); } else { this.props.history.push(`/repos/${newNamespace}/`); @@ -111,8 +115,10 @@ class Overview extends React.Component { render() { const { error, loading, showCreateButton, namespace, namespaces, t } = this.props; - - const namespacesToRender = namespaces ? ["", ...namespaces._embedded.namespaces.map(n => n.namespace).sort()] : []; + const namespaceFilterPlaceholder = this.getNamespaceFilterPlaceholder(); + const namespacesToRender = namespaces + ? [namespaceFilterPlaceholder, ...namespaces._embedded.namespaces.map(n => n.namespace).sort()] + : []; return ( @@ -126,6 +132,7 @@ class Overview extends React.Component { link="repos" label={t("overview.createButton")} testId="repository-overview" + searchPlaceholder={t("overview.searchRepository")} /> diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx index 216f33d425..fdbda0903b 100644 --- a/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/AddApiKey.tsx @@ -23,7 +23,7 @@ */ import React, { FC, useEffect, useState } from "react"; -import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton } from "@scm-manager/ui-components"; +import { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton, Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { CONTENT_TYPE_API_KEY } from "./SetApiKeys"; import { connect } from "react-redux"; @@ -105,6 +105,8 @@ const AddApiKey: FC = ({ return ( <> +
+ {newKeyModal} = ({ apiKey, onDelete }) => { if (apiKey?._links?.delete) { deleteButton = ( onDelete((apiKey._links.delete as Link).href)}> - - + + ); @@ -52,7 +52,7 @@ export const ApiKeyEntry: FC = ({ apiKey, onDelete }) => {
diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx index 8d80add477..7b9af8a415 100644 --- a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx @@ -25,11 +25,10 @@ import { Collection, Links, User, Me } from "@scm-manager/ui-types"; import React, { FC, useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components"; +import { apiClient, ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components"; import ApiKeyTable from "./ApiKeyTable"; import AddApiKey from "./AddApiKey"; import { useTranslation } from "react-i18next"; -import styled from "styled-components"; export type ApiKeysCollection = Collection & { _embedded: { @@ -51,10 +50,6 @@ type Props = { user: User | Me; }; -const Subtitle = styled.div` - margin-bottom: 1rem; -`; - const SetApiKeys: FC = ({ user }) => { const [t] = useTranslation("users"); const [error, setError] = useState(); @@ -94,14 +89,13 @@ const SetApiKeys: FC = ({ user }) => { return ( <> -
-

{t("apiKey.text1")} {t("apiKey.manageRoles")}

-

{t("apiKey.text2")}

-
-
+ +

+ {t("apiKey.text1")} {t("apiKey.manageRoles")} +

+

{t("apiKey.text2")}

+
-
-

Create new key

{createLink && } ); diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx index e0129e8245..c693d89130 100644 --- a/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx +++ b/scm-ui/ui-webapp/src/users/components/publicKeys/AddPublicKey.tsx @@ -23,7 +23,6 @@ */ import React, { FC, useState } from "react"; -import { User, Link, Links, Collection } from "@scm-manager/ui-types/src"; import { ErrorNotification, InputField, @@ -31,7 +30,8 @@ import { Textarea, SubmitButton, apiClient, - Loading + Loading, + Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { CONTENT_TYPE_PUBLIC_KEY } from "./SetPublicKeys"; @@ -77,6 +77,8 @@ const AddPublicKey: FC = ({ createLink, refresh }) => { return ( <> +
+
{t("branches.table.branches")}{t(`branches.table.branches.${type}`)}
{apiKey.displayName} {apiKey.permissionRole} - + {deleteButton}