add delete link to branchDto

This commit is contained in:
Eduard Heimbuch
2020-11-11 14:09:15 +01:00
parent 2fab771740
commit bb82c18e2b
13 changed files with 135 additions and 51 deletions

View File

@@ -22,25 +22,43 @@
* SOFTWARE. * SOFTWARE.
*/ */
import React, { FC } from "react"; import React, { FC } from "react";
import { Link } from "react-router-dom"; import { Link as ReactLink } from "react-router-dom";
import { Branch } from "@scm-manager/ui-types"; import { Branch, Link } from "@scm-manager/ui-types";
import DefaultBranchTag from "./DefaultBranchTag"; import DefaultBranchTag from "./DefaultBranchTag";
import { Icon } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
type Props = { type Props = {
baseUrl: string; baseUrl: string;
branch: Branch; branch: Branch;
onDelete: (url: string) => void;
}; };
const BranchRow: FC<Props> = ({ baseUrl, branch }) => { const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete }) => {
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`; const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
const [t] = useTranslation("repos");
let deleteButton;
if ((branch?._links?.delete as Link)?.href) {
const url = (branch._links.delete as Link).href;
deleteButton = (
<a className="level-item" onClick={() => onDelete(url)}>
<span className="icon is-small">
<Icon name="trash" className="fas" title={t("branch.delete")} />
</span>
</a>
);
}
return ( return (
<tr> <tr>
<td> <td>
<Link to={to} title={branch.name}> <ReactLink to={to} title={branch.name}>
{branch.name} {branch.name}
<DefaultBranchTag defaultBranch={branch.defaultBranch} /> <DefaultBranchTag defaultBranch={branch.defaultBranch} />
</Link> </ReactLink>
</td> </td>
<td className="is-darker">{deleteButton}</td>
</tr> </tr>
); );
}; };

View File

@@ -21,19 +21,29 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
import React from "react"; import React, { FC } from "react";
import { WithTranslation, withTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import BranchRow from "./BranchRow"; import BranchRow from "./BranchRow";
import { Branch } from "@scm-manager/ui-types"; import { Branch } from "@scm-manager/ui-types";
type Props = WithTranslation & { type Props = {
baseUrl: string; baseUrl: string;
branches: Branch[]; branches: Branch[];
onDelete: (url: string) => void;
}; };
class BranchTable extends React.Component<Props> { const BranchTable: FC<Props> = ({ baseUrl, branches, onDelete }) => {
render() { const [t] = useTranslation("repos");
const { t } = this.props;
const renderRow = () => {
let rowContent = null;
if (branches) {
rowContent = branches.map((branch, index) => {
return <BranchRow key={index} baseUrl={baseUrl} branch={branch} onDelete={onDelete} />;
});
}
return rowContent;
};
return ( return (
<table className="card-table table is-hoverable is-fullwidth is-word-break"> <table className="card-table table is-hoverable is-fullwidth is-word-break">
<thead> <thead>
@@ -41,21 +51,9 @@ class BranchTable extends React.Component<Props> {
<th>{t("branches.table.branches")}</th> <th>{t("branches.table.branches")}</th>
</tr> </tr>
</thead> </thead>
<tbody>{this.renderRow()}</tbody> <tbody>{renderRow()}</tbody>
</table> </table>
); );
} };
renderRow() { export default BranchTable;
const { baseUrl, branches } = this.props;
let rowContent = null;
if (branches) {
rowContent = branches.map((branch, index) => {
return <BranchRow key={index} baseUrl={baseUrl} branch={branch} />;
});
}
return rowContent;
}
}
export default withTranslation("repos")(BranchTable);

View File

@@ -37,6 +37,7 @@ import {
} from "../modules/branches"; } from "../modules/branches";
import { orderBranches } from "../util/orderBranches"; import { orderBranches } from "../util/orderBranches";
import BranchTable from "../components/BranchTable"; import BranchTable from "../components/BranchTable";
import { apiClient } from "@scm-manager/ui-components/src";
type Props = WithTranslation & { type Props = WithTranslation & {
repository: Repository; repository: Repository;
@@ -80,11 +81,15 @@ class BranchesOverview extends React.Component<Props> {
); );
} }
onDelete(url: string) {
apiClient.delete(url).catch(error => this.setState({ error }));
}
renderBranchesTable() { renderBranchesTable() {
const { baseUrl, branches, t } = this.props; const { baseUrl, branches, t } = this.props;
if (branches && branches.length > 0) { if (branches && branches.length > 0) {
orderBranches(branches); orderBranches(branches);
return <BranchTable baseUrl={baseUrl} branches={branches} />; return <BranchTable baseUrl={baseUrl} branches={branches} onDelete={this.onDelete} />;
} }
return <Notification type="info">{t("branches.overview.noBranches")}</Notification>; return <Notification type="info">{t("branches.overview.noBranches")}</Notification>;
} }

View File

@@ -45,6 +45,6 @@ public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToD
} }
private String createSelfLink(Repository repository, String branch) { private String createSelfLink(Repository repository, String branch) {
return resourceLinks.branch().history(repository.getNamespaceAndName(), branch); return resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch);
} }
} }

View File

@@ -55,11 +55,11 @@ public class BranchCollectionToDtoMapper {
public HalRepresentation map(Repository repository, Collection<Branch> branches) { public HalRepresentation map(Repository repository, Collection<Branch> branches) {
return new HalRepresentation( return new HalRepresentation(
createLinks(repository), createLinks(repository),
embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches))); embedDtos(getBranchDtoList(repository, branches)));
} }
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) { public List<BranchDto> getBranchDtoList(Repository repository, Collection<Branch> branches) {
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList()); return branches.stream().map(branch -> branchToDtoMapper.map(branch, repository)).collect(toList());
} }
private Links createLinks(Repository repository) { private Links createLinks(Repository repository) {

View File

@@ -134,7 +134,7 @@ public class BranchRootResource {
.stream() .stream()
.filter(branch -> branchName.equals(branch.getName())) .filter(branch -> branchName.equals(branch.getName()))
.findFirst() .findFirst()
.map(branch -> branchToDtoMapper.map(branch, namespaceAndName)) .map(branch -> branchToDtoMapper.map(branch, repositoryService.getRepository()))
.map(Response::ok) .map(Response::ok)
.orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName))) .orElseThrow(() -> notFound(entity("branch", branchName).in(namespaceAndName)))
.build(); .build();
@@ -249,7 +249,7 @@ public class BranchRootResource {
branchCommand.from(parentName); branchCommand.from(parentName);
} }
Branch newBranch = branchCommand.branch(branchName); Branch newBranch = branchCommand.branch(branchName);
return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build(); return Response.created(URI.create(resourceLinks.branch().self(namespace, name, newBranch.getName()))).build();
} }
} }

View File

@@ -32,6 +32,8 @@ import org.mapstruct.Mapping;
import org.mapstruct.ObjectFactory; import org.mapstruct.ObjectFactory;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.EdisonHalAppender; import sonia.scm.web.EdisonHalAppender;
import javax.inject.Inject; import javax.inject.Inject;
@@ -46,16 +48,21 @@ public abstract class BranchToBranchDtoMapper extends HalAppenderMapper {
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract BranchDto map(Branch branch, @Context NamespaceAndName namespaceAndName); public abstract BranchDto map(Branch branch, @Context Repository repository);
@ObjectFactory @ObjectFactory
BranchDto createDto(@Context NamespaceAndName namespaceAndName, Branch branch) { BranchDto createDto(@Context Repository repository, Branch branch) {
NamespaceAndName namespaceAndName = new NamespaceAndName(repository.getNamespace(), repository.getName());
Links.Builder linksBuilder = linkingTo() Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.branch().self(namespaceAndName, branch.getName())) .self(resourceLinks.branch().self(repository.getNamespace(), repository.getName(), branch.getName()))
.single(linkBuilder("history", resourceLinks.branch().history(namespaceAndName, branch.getName())).build()) .single(linkBuilder("history", resourceLinks.branch().history(repository.getNamespace(), repository.getName(), branch.getName())).build())
.single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()) .single(linkBuilder("changeset", resourceLinks.changeset().changeset(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build())
.single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build()); .single(linkBuilder("source", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch.getRevision())).build());
if (!branch.isDefaultBranch() && RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(linkBuilder("delete", resourceLinks.branch().delete(repository.getNamespace(), repository.getName(), branch.getName())).build());
}
Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder(); Embedded.Builder embeddedBuilder = Embedded.embeddedBuilder();
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName); applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), branch, namespaceAndName);

View File

@@ -56,7 +56,7 @@ class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper<Chan
private BranchReferenceDto createBranchReferenceDto(Repository repository, String branchName) { private BranchReferenceDto createBranchReferenceDto(Repository repository, String branchName) {
BranchReferenceDto branchReferenceDto = new BranchReferenceDto(); BranchReferenceDto branchReferenceDto = new BranchReferenceDto();
branchReferenceDto.setName(branchName); branchReferenceDto.setName(branchName);
branchReferenceDto.add(Links.linkingTo().self(resourceLinks.branch().self(repository.getNamespaceAndName(), branchName)).build()); branchReferenceDto.add(Links.linkingTo().self(resourceLinks.branch().self(repository.getNamespace(), repository.getName(), branchName)).build());
return branchReferenceDto; return branchReferenceDto;
} }
} }

View File

@@ -39,6 +39,6 @@ public class DefaultBranchLinkProvider implements BranchLinkProvider {
@Override @Override
public String get(NamespaceAndName namespaceAndName, String branch) { public String get(NamespaceAndName namespaceAndName, String branch) {
return resourceLinks.branch().self(namespaceAndName, branch); return resourceLinks.branch().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), branch);
} }
} }

View File

@@ -132,7 +132,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
} }
} }
if (repositoryService.isSupported(Command.BRANCHES)) { if (repositoryService.isSupported(Command.BRANCHES)) {
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(namespace, name, embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository,
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId())))); getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
} }

View File

@@ -25,6 +25,7 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.security.gpg.UserPublicKeyResource; import sonia.scm.security.gpg.UserPublicKeyResource;
import javax.inject.Inject; import javax.inject.Inject;
@@ -485,17 +486,21 @@ class ResourceLinks {
branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class); branchLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class);
} }
String self(NamespaceAndName namespaceAndName, String branch) { String self(String namespace, String name, String branch) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("get").parameters(branch).href(); return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("get").parameters(branch).href();
} }
public String history(NamespaceAndName namespaceAndName, String branch) { public String history(String namespace, String name, String branch) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href(); return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("history").parameters(branch).href();
} }
public String create(String namespace, String name) { public String create(String namespace, String name) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href(); return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
} }
public String delete(String namespace, String name, String branch) {
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("delete").parameters(branch).href();
}
} }
public IncomingLinks incoming() { public IncomingLinks incoming() {

View File

@@ -24,28 +24,49 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Branch; import sonia.scm.repository.Branch;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import java.net.URI; import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class BranchToBranchDtoMapperTest { class BranchToBranchDtoMapperTest {
private final URI baseUri = URI.create("https://hitchhiker.com"); private final URI baseUri = URI.create("https://hitchhiker.com/api/");
@SuppressWarnings("unused") // Is injected @SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private Subject subject;
@InjectMocks @InjectMocks
private BranchToBranchDtoMapperImpl mapper; private BranchToBranchDtoMapperImpl mapper;
@BeforeEach
void setupSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void tearDown() {
ThreadContext.unbindSubject();
}
@Test @Test
void shouldAppendLinks() { void shouldAppendLinks() {
HalEnricherRegistry registry = new HalEnricherRegistry(); HalEnricherRegistry registry = new HalEnricherRegistry();
@@ -59,7 +80,37 @@ class BranchToBranchDtoMapperTest {
Branch branch = Branch.normalBranch("master", "42"); Branch branch = Branch.normalBranch("master", "42");
BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold")); BranchDto dto = mapper.map(branch, RepositoryTestData.createHeartOfGold());
assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master"); assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/HeartOfGold/master");
} }
@Test
void shouldAppendDeleteLink() {
Repository repository = RepositoryTestData.createHeartOfGold();
when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(true);
Branch branch = Branch.normalBranch("master", "42");
BranchDto dto = mapper.map(branch, repository);
assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("https://hitchhiker.com/api/v2/repositories/hitchhiker/HeartOfGold/branches/master");
}
@Test
void shouldNotAppendDeleteLinkIfDefaultBranch() {
Repository repository = RepositoryTestData.createHeartOfGold();
Branch branch = Branch.defaultBranch("master", "42");
BranchDto dto = mapper.map(branch, repository);
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
}
@Test
void shouldNotAppendDeleteLinkIfNotPermitted() {
Repository repository = RepositoryTestData.createHeartOfGold();
when(subject.isPermitted("repository:modify:" + repository.getId())).thenReturn(false);
Branch branch = Branch.normalBranch("master", "42");
BranchDto dto = mapper.map(branch, repository);
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
}
} }

View File

@@ -140,13 +140,13 @@ public class ResourceLinksTest {
@Test @Test
public void shouldCreateCorrectBranchUrl() { public void shouldCreateCorrectBranchUrl() {
String url = resourceLinks.branch().self(new NamespaceAndName("space", "name"), "master"); String url = resourceLinks.branch().self("space", "name", "master");
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master", url); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master", url);
} }
@Test @Test
public void shouldCreateCorrectBranchHiostoryUrl() { public void shouldCreateCorrectBranchHiostoryUrl() {
String url = resourceLinks.branch().history(new NamespaceAndName("space", "name"), "master"); String url = resourceLinks.branch().history("space", "name", "master");
assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master/changesets/", url); assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/name/branches/master/changesets/", url);
} }