From 6454167b0dd95ef8497159b32af93133942ac07a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 15 Sep 2020 16:40:59 +0200 Subject: [PATCH 1/9] show modified information on branches overview --- scm-ui/ui-types/src/Branches.ts | 3 ++ scm-ui/ui-webapp/public/locales/de/repos.json | 4 +- scm-ui/ui-webapp/public/locales/en/repos.json | 4 +- .../repos/branches/components/BranchRow.tsx | 46 +++++++++++-------- .../sonia/scm/api/v2/resources/BranchDto.java | 16 +++++-- .../v2/resources/BranchToBranchDtoMapper.java | 30 +++++++++++- .../BranchToBranchDtoMapperTest.java | 39 +++++++++++++++- 7 files changed, 115 insertions(+), 27 deletions(-) diff --git a/scm-ui/ui-types/src/Branches.ts b/scm-ui/ui-types/src/Branches.ts index 87eb5af3dc..de8aa5fa49 100644 --- a/scm-ui/ui-types/src/Branches.ts +++ b/scm-ui/ui-types/src/Branches.ts @@ -23,11 +23,14 @@ */ import { Links } from "./hal"; +import { Person } from "."; export type Branch = { name: string; revision: string; defaultBranch?: boolean; + lastModified?: Date; + lastModifier?: Person; _links: Links; }; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 68e28e5d35..5b3e74f701 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -51,7 +51,9 @@ "overview": { "title": "Übersicht aller verfügbaren Branches", "noBranches": "Keine Branches gefunden.", - "createButton": "Branch erstellen" + "createButton": "Branch erstellen", + "lastModifier": "von", + "lastModified": "Aktualisiert" }, "table": { "branches": "Branches" diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index d6e003a499..eeab9a9dda 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -51,7 +51,9 @@ "overview": { "title": "Overview of all branches", "noBranches": "No branches found.", - "createButton": "Create Branch" + "createButton": "Create Branch", + "lastModifier": "by", + "lastModified": "Updated" }, "table": { "branches": "Branches" 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 866ac8d849..58bae4ceb5 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchRow.tsx @@ -21,34 +21,42 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import React from "react"; +import React, { FC } from "react"; import { Link } from "react-router-dom"; import { Branch } from "@scm-manager/ui-types"; import DefaultBranchTag from "./DefaultBranchTag"; +import { DateFromNow } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; type Props = { baseUrl: string; branch: Branch; }; -class BranchRow extends React.Component { - renderLink(to: string, label: string, defaultBranch?: boolean) { - return ( - - {label} - - ); - } +const Modified = styled.span` + margin-left: 1rem; + font-size: 0.8rem; +`; - render() { - const { baseUrl, branch } = this.props; - const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; - return ( - - {this.renderLink(to, branch.name, branch.defaultBranch)} - - ); - } -} +const BranchRow: FC = ({ baseUrl, branch }) => { + const [t] = useTranslation("repos"); + + const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; + return ( + + + + {branch.name} + + + {t("branches.overview.lastModified")} {" "} + {t("branches.overview.lastModifier")} {branch.lastModifier?.name} + + + + + ); +}; export default BranchRow; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java index c7c1a4a917..5e52d57152 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java @@ -21,9 +21,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; +import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; @@ -31,20 +32,29 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.validator.constraints.Length; + import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; +import java.time.Instant; -@Getter @Setter @NoArgsConstructor +@Getter +@Setter +@NoArgsConstructor public class BranchDto extends HalRepresentation { private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>"; private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/."; static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?"; - @NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES) + @NotEmpty + @Length(min = 1, max = 100) + @Pattern(regexp = VALID_BRANCH_NAMES) private String name; private String revision; private boolean defaultBranch; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Instant lastModified; + private PersonDto lastModifier; BranchDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java index 0d779d2164..e49a42d321 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapper.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.api.v2.resources; import de.otto.edison.hal.Embedded; @@ -30,11 +30,19 @@ import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; +import sonia.scm.ContextEntry; import sonia.scm.repository.Branch; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; +import java.io.IOException; +import java.time.Instant; import static de.otto.edison.hal.Link.linkBuilder; import static de.otto.edison.hal.Links.linkingTo; @@ -45,9 +53,14 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper { @Inject private ResourceLinks resourceLinks; + @Inject + private RepositoryServiceFactory serviceFactory; + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName); + abstract PersonDto map(Person person); + @ObjectFactory BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) { Links.Builder linksBuilder = linkingTo() @@ -58,7 +71,20 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper { Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder(); applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName); + BranchDto branchDto = new BranchDto(linksBuilder.build(), embeddedBuilder.build()); - return new BranchDto(linksBuilder.build(), embeddedBuilder.build()); + try (RepositoryService service = serviceFactory.create(namespaceAndName)) { + Changeset latestChangeset = service.getLogCommand().setBranch(branch.getName()).getChangesets().getChangesets().get(0); + branchDto.setLastModified(Instant.ofEpochMilli(latestChangeset.getDate())); + branchDto.setLastModifier(map(latestChangeset.getAuthor())); + } catch (IOException e) { + throw new InternalRepositoryException( + ContextEntry.ContextBuilder.entity(Branch.class, branch.getName()), + "Could not read latest changeset for branch", + e + ); + } + + return branchDto; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java index c9b9ea769f..1a64c46110 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchToBranchDtoMapperTest.java @@ -21,19 +21,32 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; +import com.google.common.collect.ImmutableList; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.Branch; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.PersonTestData; +import sonia.scm.repository.api.LogCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import java.io.IOException; import java.net.URI; +import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class BranchToBranchDtoMapperTest { @@ -43,6 +56,13 @@ class BranchToBranchDtoMapperTest { @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService repositoryService; + @Mock(answer = Answers.RETURNS_SELF) + private LogCommandBuilder logCommandBuilder; + @InjectMocks private BranchToBranchDtoMapperImpl mapper; @@ -63,4 +83,21 @@ class BranchToBranchDtoMapperTest { assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master"); } + @Test + void shouldMapLastChangeDateAndLastModifier() throws IOException { + long creationTime = 1000000000; + Changeset changeset = new Changeset("1", 1L, PersonTestData.ZAPHOD); + changeset.setDate(creationTime); + + when(serviceFactory.create(any(NamespaceAndName.class))).thenReturn(repositoryService); + when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(new ChangesetPagingResult(1, ImmutableList.of(changeset))); + Branch branch = Branch.normalBranch("master", "42"); + + BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold")); + + assertThat(dto.getLastModified()).isEqualTo(Instant.ofEpochMilli(creationTime)); + assertThat(dto.getLastModifier().getName()).isEqualTo(PersonTestData.ZAPHOD.getName()); + } + } From f9b3d145416e2bbb8398ea95e5c34cd92a530ac1 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 16 Sep 2020 12:45:22 +0200 Subject: [PATCH 2/9] add tag overview for repository --- scm-ui/ui-types/src/Tags.ts | 1 + scm-ui/ui-webapp/public/locales/de/repos.json | 14 +++ scm-ui/ui-webapp/public/locales/en/repos.json | 14 +++ .../src/repos/containers/RepositoryRoot.tsx | 27 +++++ .../src/repos/tags/components/TagDetail.tsx | 49 ++++++++++ .../src/repos/tags/components/TagRow.tsx | 60 ++++++++++++ .../src/repos/tags/components/TagTable.tsx | 60 ++++++++++++ .../src/repos/tags/components/TagView.tsx | 54 ++++++++++ .../src/repos/tags/container/TagRoot.tsx | 98 +++++++++++++++++++ .../src/repos/tags/container/TagsOverview.tsx | 80 +++++++++++++++ .../src/repos/tags/orderTags.test.ts | 52 ++++++++++ scm-ui/ui-webapp/src/repos/tags/orderTags.ts | 32 ++++++ 12 files changed, 541 insertions(+) create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts create mode 100644 scm-ui/ui-webapp/src/repos/tags/orderTags.ts diff --git a/scm-ui/ui-types/src/Tags.ts b/scm-ui/ui-types/src/Tags.ts index 6fc1973150..83ed4c3860 100644 --- a/scm-ui/ui-types/src/Tags.ts +++ b/scm-ui/ui-types/src/Tags.ts @@ -27,5 +27,6 @@ import { Links } from "./hal"; export type Tag = { name: string; revision: string; + date: Date; _links: Links; }; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 5b3e74f701..0b63dc7c9e 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -31,6 +31,7 @@ "navigationLabel": "Repository", "informationNavLink": "Informationen", "branchesNavLink": "Branches", + "tagsNavLink": "Tags", "sourcesNavLink": "Code", "settingsNavLink": "Einstellungen", "generalNavLink": "Generell", @@ -71,6 +72,19 @@ "sources": "Sources", "defaultTag": "Default" }, + "tags": { + "overview": { + "title": "Übersicht aller verfügbaren Tags", + "noTags": "Keine Tags gefunden.", + "created": "Erstellt" + }, + "table": { + "tags": "Tags" + } + }, + "tag": { + "name": "Name:" + }, "code": { "sources": "Sources", "commits": "Commits", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index eeab9a9dda..033afe4ffd 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -31,6 +31,7 @@ "navigationLabel": "Repository", "informationNavLink": "Information", "branchesNavLink": "Branches", + "tagsNavLink": "Tags", "sourcesNavLink": "Code", "settingsNavLink": "Settings", "generalNavLink": "General", @@ -71,6 +72,19 @@ "sources": "Sources", "defaultTag": "Default" }, + "tags": { + "overview": { + "title": "Overview of all tags", + "noTags": "No tags found.", + "created": "Created" + }, + "table": { + "tags": "Tags" + } + }, + "tag": { + "name": "Name:" + }, "code": { "sources": "Sources", "commits": "Commits", diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index bcbb217fed..f3ef73bf36 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -54,6 +54,8 @@ import CodeOverview from "../codeSection/containers/CodeOverview"; import ChangesetView from "./ChangesetView"; import SourceExtensions from "../sources/containers/SourceExtensions"; import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components"; +import TagsOverview from "../tags/container/TagsOverview"; +import TagRoot from "../tags/container/TagRoot"; type Props = RouteComponentProps & WithTranslation & { @@ -99,6 +101,12 @@ class RepositoryRoot extends React.Component { return route.location.pathname.match(regex); }; + matchesTags = (route: any) => { + const url = this.matchedUrl(); + const regex = new RegExp(`${url}/tag/.+/info`); + return route.location.pathname.match(regex); + }; + matchesCode = (route: any) => { const url = this.matchedUrl(); const regex = new RegExp(`${url}(/code)/.*`); @@ -245,6 +253,15 @@ class RepositoryRoot extends React.Component { render={() => } /> } /> + } + /> + } + /> @@ -267,6 +284,16 @@ class RepositoryRoot extends React.Component { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.branchesNavLink")} /> + = ({ tag }) => { + const [t] = useTranslation("repos"); + + if (!tag) { + return null; + } + + return ( +
+
+ {t("tag.name")} {tag?.name} +
+
+ ); +}; + +export default TagDetail; diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx new file mode 100644 index 0000000000..00b18da623 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx @@ -0,0 +1,60 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { Tag } from "@scm-manager/ui-types"; +import styled from "styled-components"; +import { DateFromNow } from "@scm-manager/ui-components"; + +type Props = { + tag: Tag; + baseUrl: string; +}; + +const Created = styled.span` + margin-left: 1rem; + font-size: 0.8rem; +`; + +const TagRow: FC = ({ tag, baseUrl }) => { + const [t] = useTranslation("repos"); + + const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`; + return ( + + + + {tag.name} + + {t("tags.overview.created")} + + + + + ); +}; + +export default TagRow; diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx new file mode 100644 index 0000000000..4cd694766f --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx @@ -0,0 +1,60 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { Tag } from "@scm-manager/ui-types"; +import { useTranslation } from "react-i18next"; +import TagRow from "./TagRow"; + +type Props = { + baseUrl: string; + tags: Tag[]; +}; + +const TagTable: FC = ({ baseUrl, tags }) => { + const [t] = useTranslation("repos"); + + const renderRow = () => { + let rowContent = null; + if (tags) { + rowContent = tags.map((tag, index) => { + return ; + }); + } + return rowContent; + }; + + return ( + + + + + + + {renderRow()} +
{t("tags.table.tags")}
+ ); +}; + +export default TagTable; diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx new file mode 100644 index 0000000000..82ce697bc1 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx @@ -0,0 +1,54 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { Repository, Tag } from "@scm-manager/ui-types"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import TagDetail from "./TagDetail"; + +type Props = { + repository: Repository; + tag?: Tag; +}; + +const TagView: FC = ({ repository, tag }) => { + return ( +
+ +
+
+ +
+
+ ); +}; + +export default TagView; diff --git a/scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx b/scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx new file mode 100644 index 0000000000..55f45f01c4 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx @@ -0,0 +1,98 @@ +/* + * 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. + */ + +import React, { FC, useEffect, useState } from "react"; +import { Link, Repository, Tag } from "@scm-manager/ui-types"; +import { Redirect, Switch, useLocation, useRouteMatch, Route } from "react-router-dom"; +import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import TagView from "../components/TagView"; + +type Props = { + repository: Repository; + baseUrl: string; +}; + +const TagRoot: FC = ({ repository, baseUrl }) => { + const match = useRouteMatch(); + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + const [tag, setTag] = useState(); + + useEffect(() => { + const link = (repository._links?.tags as Link)?.href; + if (link) { + setLoading(true); + apiClient + .get(link) + .then(r => r.json()) + .then(r => setTags(r._embedded.tags)) + .catch(setError); + } + }, [repository]); + + useEffect(() => { + const tagName = match?.params?.tag; + const link = tags && tags.length > 0 && (tags.find(tag => tag.name === tagName)?._links.self as Link).href; + if (link) { + apiClient + .get(link) + .then(r => r.json()) + .then(setTag) + .then(() => setLoading(false)) + .catch(setError); + } + }, [tags]); + + const stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 1); + } + return url; + }; + + const matchedUrl = () => { + return stripEndingSlash(match.url); + }; + + if (error) { + return ; + } + + if (loading || !tags) { + return ; + } + + const url = matchedUrl(); + + return ( + + + } /> + + ); +}; + +export default TagRoot; diff --git a/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx b/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx new file mode 100644 index 0000000000..35b771ae1a --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx @@ -0,0 +1,80 @@ +/* + * 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. + */ + +import React, { FC, useEffect, useState } from "react"; +import { Repository, Tag, Link } from "@scm-manager/ui-types"; +import { ErrorNotification, Loading, Notification, Subtitle, apiClient } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import orderTags from "../orderTags"; +import TagTable from "../components/TagTable"; + +type Props = { + repository: Repository; + baseUrl: string; +}; + +const TagsOverview: FC = ({ repository, baseUrl }) => { + const [t] = useTranslation("repos"); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const [tags, setTags] = useState([]); + + useEffect(() => { + const link = (repository._links?.tags as Link)?.href; + if (link) { + setLoading(true); + apiClient + .get(link) + .then(r => r.json()) + .then(r => setTags(r._embedded.tags)) + .then(() => setLoading(false)) + .catch(setError); + } + }, [repository]); + + const renderTagsTable = () => { + if (!loading && tags && tags.length > 0) { + orderTags(tags); + return ; + } + return {t("tags.overview.noTags")}; + }; + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return ( + <> + + {renderTagsTable()} + + ); +}; + +export default TagsOverview; diff --git a/scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts b/scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts new file mode 100644 index 0000000000..3b8667e21f --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +import orderTags from "./orderTags"; + +const tag1 = { + name: "tag1", + revision: "revision1", + date: new Date(2020, 1, 1), + _links: {} +}; +const tag2 = { + name: "tag2", + revision: "revision2", + date: new Date(2020, 1, 3), + _links: {} +}; +const tag3 = { + name: "tag3", + revision: "revision3", + date: new Date(2020, 1, 2), + _links: {} +}; + +describe("order tags", () => { + it("should order tags descending by date", () => { + const tags = [tag1, tag2, tag3]; + orderTags(tags); + expect(tags).toEqual([tag2, tag3, tag1]); + }); +}); diff --git a/scm-ui/ui-webapp/src/repos/tags/orderTags.ts b/scm-ui/ui-webapp/src/repos/tags/orderTags.ts new file mode 100644 index 0000000000..87697c260a --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/orderTags.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +// sort tags by date beginning with latest first +import { Tag } from "@scm-manager/ui-types"; + +export default function orderTags(tags: Tag[]) { + tags.sort((a, b) => { + return new Date(b.date) - new Date(a.date); + }); +} From 909f5ebec9b64dd383c96e7dedeb6671e05cb9a1 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 16 Sep 2020 14:11:27 +0200 Subject: [PATCH 3/9] add instructions to checkout tags --- .../src/main/js/GitTagInformation.tsx | 52 ++++++++++++++++++ .../scm-git-plugin/src/main/js/index.ts | 2 + .../main/resources/locales/de/plugins.json | 1 + .../main/resources/locales/en/plugins.json | 1 + .../src/main/js/HgTagInformation.tsx | 50 +++++++++++++++++ .../scm-hg-plugin/src/main/js/index.ts | 2 + .../main/resources/locales/de/plugins.json | 3 +- .../main/resources/locales/en/plugins.json | 3 +- scm-ui/ui-webapp/public/locales/de/repos.json | 4 +- scm-ui/ui-webapp/public/locales/en/repos.json | 4 +- .../repos/tags/components/TagButtonGroup.tsx | 53 +++++++++++++++++++ .../src/repos/tags/components/TagDetail.tsx | 24 +++++++-- .../src/repos/tags/components/TagView.tsx | 2 +- 13 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/js/GitTagInformation.tsx create mode 100644 scm-plugins/scm-hg-plugin/src/main/js/HgTagInformation.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagButtonGroup.tsx diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitTagInformation.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitTagInformation.tsx new file mode 100644 index 0000000000..dc59f0060a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/js/GitTagInformation.tsx @@ -0,0 +1,52 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { Tag } from "@scm-manager/ui-types"; +import { useTranslation } from "react-i18next"; + +type Props = { + tag: Tag; +}; + +const GitTagInformation: FC = ({ tag }) => { + const [t] = useTranslation("plugins"); + + if (!tag) { + return null; + } + + return ( + <> +

{t("scm-git-plugin.information.checkoutTag")}

+
+        
+          git checkout tags/{tag?.name} -b branch/{tag?.name}
+        
+      
+ + ); +}; + +export default GitTagInformation; diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.ts b/scm-plugins/scm-git-plugin/src/main/js/index.ts index dc7f8d11c8..49d85cb8bb 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-git-plugin/src/main/js/index.ts @@ -32,6 +32,7 @@ import GitGlobalConfiguration from "./GitGlobalConfiguration"; import GitBranchInformation from "./GitBranchInformation"; import GitMergeInformation from "./GitMergeInformation"; import RepositoryConfig from "./RepositoryConfig"; +import GitTagInformation from "./GitTagInformation"; // repository @@ -42,6 +43,7 @@ export const gitPredicate = (props: any) => { binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate); binder.bind("repos.branch-details.information", GitBranchInformation, gitPredicate); +binder.bind("repos.tag-details.information", GitTagInformation, gitPredicate); binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate); binder.bind("repos.repository-avatar", GitAvatar, gitPredicate); diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json index e150ceb42b..7553856e57 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json @@ -6,6 +6,7 @@ "replace": "Ein bestehendes Repository aktualisieren", "fetch": "Remote-Änderungen herunterladen", "checkout": "Branch wechseln", + "checkoutTag": "Tag als neuen Branch auschecken", "merge": { "heading": "Merge des Source Branch in den Target Branch", "checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.", diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index 22eb46b9f8..23ab1ffce0 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -6,6 +6,7 @@ "replace": "Push an existing repository", "fetch": "Get remote changes", "checkout": "Switch branch", + "checkoutTag": "Checkout tag as new branch", "merge": { "heading": "How to merge source branch into target branch", "checkout": "1. Make sure your workspace is clean and checkout target branch", diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgTagInformation.tsx b/scm-plugins/scm-hg-plugin/src/main/js/HgTagInformation.tsx new file mode 100644 index 0000000000..99c7f47d08 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgTagInformation.tsx @@ -0,0 +1,50 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Tag } from "@scm-manager/ui-types"; + +type Props = { + tag?: Tag; +}; + +const HgTagInformation: FC = ({ tag }) => { + const [t] = useTranslation("plugins"); + + if (!tag) { + return null; + } + + return ( + <> +

{t("scm-hg-plugin.information.checkoutTag")}

+
+        hg update {tag?.name}
+      
+ + ); +}; + +export default HgTagInformation; diff --git a/scm-plugins/scm-hg-plugin/src/main/js/index.ts b/scm-plugins/scm-hg-plugin/src/main/js/index.ts index 6fed874389..2365b1458a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-hg-plugin/src/main/js/index.ts @@ -28,6 +28,7 @@ import HgAvatar from "./HgAvatar"; import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components"; import HgGlobalConfiguration from "./HgGlobalConfiguration"; import HgBranchInformation from "./HgBranchInformation"; +import HgTagInformation from "./HgTagInformation"; const hgPredicate = (props: any) => { return props.repository && props.repository.type === "hg"; @@ -35,6 +36,7 @@ const hgPredicate = (props: any) => { binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate); binder.bind("repos.branch-details.information", HgBranchInformation, hgPredicate); +binder.bind("repos.tag-details.information", HgTagInformation, hgPredicate); binder.bind("repos.repository-avatar", HgAvatar, hgPredicate); // bind global configuration diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json index d32847a3af..6a4a639812 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/de/plugins.json @@ -5,7 +5,8 @@ "create" : "Neues Repository erstellen", "replace" : "Ein bestehendes Repository aktualisieren", "fetch": "Remote-Änderungen herunterladen", - "checkout": "Branch wechseln" + "checkout": "Branch wechseln", + "checkoutTag": "Tag auschecken" }, "config": { "link": "Mercurial", diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index 3792bd4a47..87f4d80ac4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -5,7 +5,8 @@ "create" : "Create a new repository", "replace" : "Push an existing repository", "fetch": "Get remote changes", - "checkout": "Switch branch" + "checkout": "Switch branch", + "checkoutTag": "Checkout tag" }, "config": { "link": "Mercurial", diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 0b63dc7c9e..3467e61a7e 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -83,7 +83,9 @@ } }, "tag": { - "name": "Name:" + "name": "Tag-Name", + "commit": "Commit", + "sources": "Sources" }, "code": { "sources": "Sources", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 033afe4ffd..34b161bc6e 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -83,7 +83,9 @@ } }, "tag": { - "name": "Name:" + "name": "Tag-Name", + "commit": "Commit", + "sources": "Sources" }, "code": { "sources": "Sources", diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagButtonGroup.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagButtonGroup.tsx new file mode 100644 index 0000000000..689caac1e4 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagButtonGroup.tsx @@ -0,0 +1,53 @@ +/* + * 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. + */ + +import React, { FC } from "react"; +import { Tag, Repository } from "@scm-manager/ui-types"; +import { Button, ButtonAddons } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; + +type Props = { + repository: Repository; + tag: Tag; +}; + +const TagButtonGroup: FC = ({ repository, tag }) => { + const [t] = useTranslation("repos"); + + const changesetLink = `/repo/${repository.namespace}/${repository.name}/code/changeset/${encodeURIComponent( + tag.revision + )}`; + const sourcesLink = `/repo/${repository.namespace}/${repository.name}/sources/${encodeURIComponent(tag.revision)}/`; + + return ( + <> + +