This commit is contained in:
Johannes Schnatterer
2018-10-09 11:26:08 +02:00
135 changed files with 4010 additions and 21267 deletions

View File

@@ -188,15 +188,14 @@
<scope>test</scope>
</dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId>
<version>${ssp.version}</version>
</dependency>
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId>
<version>${ssp.version}</version>
<optional>true</optional>
@@ -765,7 +764,7 @@
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
<!-- security libraries -->
<ssp.version>967c8fd521</ssp.version>
<ssp.version>1.1.0</ssp.version>
<shiro.version>1.4.0</shiro.version>
<!-- repostitory libraries -->

View File

@@ -94,6 +94,12 @@
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>test</scope>
</dependency>
<!-- json -->
<dependency>
@@ -160,14 +166,13 @@
<scope>provided</scope>
</dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId>
</dependency>
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId>
<optional>true</optional>
</dependency>

View File

@@ -4,11 +4,11 @@ import java.net.URI;
public interface ScmPathInfo {
String REST_API_PATH = "/api/rest";
String REST_API_PATH = "/api";
URI getApiRestUri();
default URI getRootUri() {
return getApiRestUri().resolve("../..");
return getApiRestUri().resolve("..");
}
}

View File

@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
@StaticPermissions(
value = "configuration",
permissions = {"read", "write"},
globalPermissions = {}
globalPermissions = {"list"}
)
public interface Configuration extends PermissionObject {
}

View File

@@ -1,125 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.filter;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Priority;
import sonia.scm.util.WebUtil;
import sonia.scm.web.filter.HttpFilter;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Filter for gzip encoding.
*
* @author Sebastian Sdorra
* @since 1.15
*/
@Priority(Filters.PRIORITY_PRE_BASEURL)
@WebElement(value = Filters.PATTERN_RESOURCE_REGEX, regex = true)
public class GZipFilter extends HttpFilter
{
/**
* the logger for GZipFilter
*/
private static final Logger logger =
LoggerFactory.getLogger(GZipFilter.class);
//~--- get methods ----------------------------------------------------------
/**
* Return the configuration for the gzip filter.
*
*
* @return gzip filter configuration
*/
public GZipFilterConfig getConfig()
{
return config;
}
//~--- methods --------------------------------------------------------------
/**
* Encodes the response, if the request has support for gzip encoding.
*
*
* @param request http request
* @param response http response
* @param chain filter chain
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilter(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (WebUtil.isGzipSupported(request))
{
if (logger.isTraceEnabled())
{
logger.trace("compress output with gzip");
}
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response,
config);
chain.doFilter(request, wrappedResponse);
wrappedResponse.finishResponse();
}
else
{
chain.doFilter(request, response);
}
}
//~--- fields ---------------------------------------------------------------
/** gzip filter configuration */
private GZipFilterConfig config = new GZipFilterConfig();
}

View File

@@ -0,0 +1,24 @@
package sonia.scm.filter;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.util.WebUtil;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
@Provider
@Slf4j
public class GZipResponseFilter implements ContainerResponseFilter {
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
if (WebUtil.isGzipSupported(requestContext::getHeaderString)) {
log.trace("compress output with gzip");
GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream());
responseContext.getHeaders().add("Content-Encoding", "gzip");
responseContext.setEntityStream(wrappedResponse);
}
}
}

View File

@@ -60,7 +60,7 @@ import java.util.List;
*
* @author Sebastian Sdorra
*/
@StaticPermissions("group")
@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
@XmlRootElement(name = "groups")
@XmlAccessorType(XmlAccessType.FIELD)
public class Group extends BasicPropertiesAware

View File

@@ -82,6 +82,22 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
{
this.total = total;
this.changesets = changesets;
this.branchName = null;
}
/**
* Constructs a new changeset paging result for a specific branch.
*
*
* @param total total number of changesets
* @param changesets current list of fetched changesets
* @param branchName branch name this result was created for
*/
public ChangesetPagingResult(int total, List<Changeset> changesets, String branchName)
{
this.total = total;
this.changesets = changesets;
this.branchName = branchName;
}
//~--- methods --------------------------------------------------------------
@@ -158,6 +174,7 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
return MoreObjects.toStringHelper(this)
.add("changesets", changesets)
.add("total", total)
.add("branch", branchName)
.toString();
//J+
}
@@ -186,37 +203,35 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
return total;
}
//~--- set methods ----------------------------------------------------------
/**
* Sets the current list of changesets.
*
*
* @param changesets current list of changesets
*/
public void setChangesets(List<Changeset> changesets)
void setChangesets(List<Changeset> changesets)
{
this.changesets = changesets;
}
/**
* Sets the total number of changesets
*
*
* @param total total number of changesets
*/
public void setTotal(int total)
void setTotal(int total)
{
this.total = total;
}
void setBranchName(String branchName) {
this.branchName = branchName;
}
/**
* Returns the branch name this result was created for. This can either be an explicit branch ("give me all
* changesets for branch xyz") or an implicit one ("give me the changesets for the default").
*/
public String getBranchName() {
return branchName;
}
//~--- fields ---------------------------------------------------------------
/** current list of changesets */
@XmlElement(name = "changeset")
@XmlElementWrapper(name = "changesets")
private List<Changeset> changesets;
/** total number of changesets */
private int total;
private String branchName;
}

View File

@@ -55,11 +55,10 @@ import java.security.Principal;
*
* @author Sebastian Sdorra
*/
@StaticPermissions("user")
@StaticPermissions(value = "user", globalPermissions = {"create", "list"})
@XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD)
public class
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
{
/** Field description */

View File

@@ -49,6 +49,7 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -266,7 +267,12 @@ public final class WebUtil
*/
public static boolean isGzipSupported(HttpServletRequest request)
{
String enc = request.getHeader(HEADER_ACCEPTENCODING);
return isGzipSupported(request::getHeader);
}
public static boolean isGzipSupported(Function<String, String> headerResolver)
{
String enc = headerResolver.apply(HEADER_ACCEPTENCODING);
return (enc != null) && enc.contains("gzip");
}

View File

@@ -0,0 +1,40 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;
public abstract class JsonEnricherBase implements JsonEnricher {
private final ObjectMapper objectMapper;
protected JsonEnricherBase(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
return mediaType.equals(context.getResponseMediaType().toString());
}
protected JsonNode value(Object object) {
return objectMapper.convertValue(object, JsonNode.class);
}
protected ObjectNode createObject() {
return objectMapper.createObjectNode();
}
protected ObjectNode createObject(Map<String, Object> values) {
ObjectNode object = createObject();
values.forEach((key, value) -> object.set(key, value(value)));
return object;
}
protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) {
((ObjectNode) parent).set(newKey, child);
}
}

View File

@@ -15,6 +15,7 @@ public class VndMediaType {
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION;
public static final String INDEX = PREFIX + "index" + SUFFIX;
public static final String USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;

View File

@@ -114,7 +114,7 @@ public class InitializingHttpScmProtocolWrapperTest {
}
private OngoingStubbing<ScmPathInfo> mockSetPathInfo() {
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/"));
}
}

View File

@@ -0,0 +1,51 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
import javax.ws.rs.core.MediaType;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
public class JsonEnricherBaseTest {
private ObjectMapper objectMapper = new ObjectMapper();
private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper);
@Test
public void testResultHasMediaType() {
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null);
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue();
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
}
@Test
public void testAppendLink() {
ObjectNode root = objectMapper.createObjectNode();
ObjectNode links = objectMapper.createObjectNode();
root.set("_links", links);
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root);
enricher.enrich(context);
assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link");
}
private static class TestJsonEnricher extends JsonEnricherBase {
public TestJsonEnricher(ObjectMapper objectMapper) {
super(objectMapper);
}
@Override
public void enrich(JsonEnricherContext context) {
JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link")));
addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode);
}
}
}

View File

@@ -0,0 +1,48 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import org.apache.http.HttpStatus;
import org.junit.Test;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.web.VndMediaType;
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
import static sonia.scm.it.utils.RestUtil.given;
public class IndexITCase {
@Test
public void shouldLinkEverythingForAdmin() {
given(VndMediaType.INDEX)
.when()
.get(RestUtil.createResourceUrl(""))
.then()
.statusCode(HttpStatus.SC_OK)
.body(
"_links.repositories.href", matchesPattern(".+/repositories/"),
"_links.users.href", matchesPattern(".+/users/"),
"_links.groups.href", matchesPattern(".+/groups/"),
"_links.config.href", matchesPattern(".+/config"),
"_links.gitConfig.href", matchesPattern(".+/config/git"),
"_links.hgConfig.href", matchesPattern(".+/config/hg"),
"_links.svnConfig.href", matchesPattern(".+/config/svn")
);
}
@Test
public void shouldCreateLoginLinksForAnonymousAccess() {
RestAssured.given() // do not specify user credentials
.when()
.get(RestUtil.createResourceUrl(""))
.then()
.statusCode(HttpStatus.SC_OK)
.body(
"_links.login.href", matchesPattern(".+/auth/.+")
);
}
}

View File

@@ -62,18 +62,4 @@ public class MeITCase {
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(403);
}
}

View File

@@ -36,6 +36,7 @@ package sonia.scm.it;
import org.apache.http.HttpStatus;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

View File

@@ -93,21 +93,4 @@ public class UserITCase {
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(403);
}
}

View File

@@ -24,6 +24,6 @@ public class RegExMatcher extends BaseMatcher<String> {
@Override
public boolean matches(Object o) {
return Pattern.compile(pattern).matcher(o.toString()).matches();
return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
}
}

View File

@@ -10,7 +10,7 @@ import static java.net.URI.create;
public class RestUtil {
public static final URI BASE_URL = create("http://localhost:8081/scm/");
public static final URI REST_BASE_URL = BASE_URL.resolve("api/rest/v2/");
public static final URI REST_BASE_URL = BASE_URL.resolve("api/v2/");
public static URI createResourceUrl(String path) {
return REST_BASE_URL.resolve(path);

View File

@@ -0,0 +1,40 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.web.JsonEnricherBase;
import sonia.scm.web.JsonEnricherContext;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Collections.singletonMap;
import static sonia.scm.web.VndMediaType.INDEX;
@Extension
public class GitConfigInIndexResource extends JsonEnricherBase {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public GitConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
super(objectMapper);
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
.method("get")
.parameters()
.href();
JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl)));
addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode);
}
}
}

View File

@@ -39,7 +39,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -51,8 +50,8 @@ import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -130,27 +129,9 @@ public class GitChangesetConverter implements Closeable
*
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit) throws IOException
public Changeset createChangeset(RevCommit commit)
{
List<String> branches = Lists.newArrayList();
Set<Ref> refs = repository.getAllRefsByPeeledObjectId().get(commit.getId());
if (Util.isNotEmpty(refs))
{
for (Ref ref : refs)
{
String branch = GitUtil.getBranch(ref);
if (branch != null)
{
branches.add(branch);
}
}
}
return createChangeset(commit, branches);
return createChangeset(commit, Collections.emptyList());
}
/**
@@ -165,7 +146,6 @@ public class GitChangesetConverter implements Closeable
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit, String branch)
throws IOException
{
return createChangeset(commit, Lists.newArrayList(branch));
}
@@ -183,7 +163,6 @@ public class GitChangesetConverter implements Closeable
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit, List<String> branches)
throws IOException
{
String id = commit.getId().name();
List<String> parentList = null;

View File

@@ -63,8 +63,11 @@ import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static java.util.Optional.of;
//~--- JDK imports ------------------------------------------------------------
/**
@@ -345,12 +348,11 @@ public final class GitUtil
*
* @throws IOException
*/
public static ObjectId getBranchId(org.eclipse.jgit.lib.Repository repo,
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
String branchName)
throws IOException
{
ObjectId branchId = null;
Ref ref = null;
if (!branchName.startsWith(REF_HEAD))
{
branchName = PREFIX_HEADS.concat(branchName);
@@ -360,24 +362,19 @@ public final class GitUtil
try
{
Ref ref = repo.findRef(branchName);
ref = repo.findRef(branchName);
if (ref != null)
{
branchId = ref.getObjectId();
}
else if (logger.isWarnEnabled())
if (ref == null)
{
logger.warn("could not find branch for {}", branchName);
}
}
catch (IOException ex)
{
logger.warn("error occured during resolve of branch id", ex);
}
return branchId;
return ref;
}
/**
@@ -499,68 +496,48 @@ public final class GitUtil
return ref;
}
/**
* Method description
*
*
* @param repo
*
* @return
*
* @throws IOException
*/
public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo)
throws IOException
{
ObjectId id = null;
String head = null;
Map<String, Ref> refs = repo.getAllRefs();
public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) {
return getRepositoryHeadRef(repo).map(Ref::getObjectId).orElse(null);
}
for (Map.Entry<String, Ref> e : refs.entrySet())
{
String key = e.getKey();
public static Optional<Ref> getRepositoryHeadRef(org.eclipse.jgit.lib.Repository repo) {
Optional<Ref> foundRef = findMostAppropriateHead(repo.getAllRefs());
if (REF_HEAD.equals(key))
{
head = REF_HEAD;
id = e.getValue().getObjectId();
break;
}
else if (key.startsWith(REF_HEAD_PREFIX))
{
id = e.getValue().getObjectId();
head = key.substring(REF_HEAD_PREFIX.length());
if (REF_MASTER.equals(head))
{
break;
}
if (foundRef.isPresent()) {
if (logger.isDebugEnabled()) {
logger.debug("use {}:{} as repository head for directory {}",
foundRef.map(GitUtil::getBranch).orElse(null),
foundRef.map(Ref::getObjectId).map(ObjectId::name).orElse(null),
repo.getDirectory());
}
} else {
logger.warn("could not find repository head in directory {}", repo.getDirectory());
}
if (id == null)
{
id = repo.resolve(Constants.HEAD);
return foundRef;
}
private static Optional<Ref> findMostAppropriateHead(Map<String, Ref> refs) {
Ref refHead = refs.get(REF_HEAD);
if (refHead != null && refHead.isSymbolic() && isBranch(refHead.getTarget().getName())) {
return of(refHead.getTarget());
}
if (logger.isDebugEnabled())
{
if ((head != null) && (id != null))
{
logger.debug("use {}:{} as repository head", head, id.name());
}
else if (id != null)
{
logger.debug("use {} as repository head", id.name());
}
else
{
logger.warn("could not find repository head");
}
Ref master = refs.get(REF_HEAD_PREFIX + REF_MASTER);
if (master != null) {
return of(master);
}
return id;
Ref develop = refs.get(REF_HEAD_PREFIX + "develop");
if (develop != null) {
return of(develop);
}
return refs.entrySet()
.stream()
.filter(e -> e.getKey().startsWith(REF_HEAD_PREFIX))
.map(Map.Entry::getValue)
.findFirst();
}
/**
@@ -648,7 +625,7 @@ public final class GitUtil
return tagName;
}
/**
* Method description
*

View File

@@ -34,19 +34,20 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.eclipse.jgit.lib.Repository;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitConstants;
import sonia.scm.repository.GitUtil;
import java.io.IOException;
import java.util.Optional;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
@@ -97,27 +98,29 @@ public class AbstractGitCommand
}
return commit;
}
protected ObjectId getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
ObjectId head;
if ( Strings.isNullOrEmpty(requestedBranch) ) {
head = getDefaultBranch(gitRepository);
} else {
head = GitUtil.getBranchId(gitRepository, requestedBranch);
}
return head;
}
protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException {
ObjectId head;
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
if (!Strings.isNullOrEmpty(defaultBranchName)) {
head = GitUtil.getBranchId(gitRepository, defaultBranchName);
Ref ref = getBranchOrDefault(gitRepository, null);
if (ref == null) {
return null;
} else {
logger.trace("no default branch configured, use repository head as default");
head = GitUtil.getRepositoryHead(gitRepository);
return ref.getObjectId();
}
}
protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
if ( Strings.isNullOrEmpty(requestedBranch) ) {
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
if (!Strings.isNullOrEmpty(defaultBranchName)) {
return GitUtil.getBranchId(gitRepository, defaultBranchName);
} else {
logger.trace("no default branch configured, use repository head as default");
Optional<Ref> repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository);
return repositoryHeadRef.orElse(null);
}
} else {
return GitUtil.getBranchId(gitRepository, requestedBranch);
}
return head;
}
//~--- fields ---------------------------------------------------------------

View File

@@ -39,6 +39,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -170,8 +171,8 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
GitChangesetConverter converter = null;
RevWalk revWalk = null;
try (org.eclipse.jgit.lib.Repository gr = open()) {
if (!gr.getAllRefs().isEmpty()) {
try (org.eclipse.jgit.lib.Repository repository = open()) {
if (!repository.getAllRefs().isEmpty()) {
int counter = 0;
int start = request.getPagingStart();
@@ -188,18 +189,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
ObjectId startId = null;
if (!Strings.isNullOrEmpty(request.getStartChangeset())) {
startId = gr.resolve(request.getStartChangeset());
startId = repository.resolve(request.getStartChangeset());
}
ObjectId endId = null;
if (!Strings.isNullOrEmpty(request.getEndChangeset())) {
endId = gr.resolve(request.getEndChangeset());
endId = repository.resolve(request.getEndChangeset());
}
revWalk = new RevWalk(gr);
revWalk = new RevWalk(repository);
converter = new GitChangesetConverter(gr, revWalk);
converter = new GitChangesetConverter(repository, revWalk);
if (!Strings.isNullOrEmpty(request.getPath())) {
revWalk.setTreeFilter(
@@ -207,13 +208,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
}
ObjectId head = getBranchOrDefault(gr, request.getBranch());
Ref branch = getBranchOrDefault(repository,request.getBranch());
if (head != null) {
if (branch != null) {
if (startId != null) {
revWalk.markStart(revWalk.lookupCommit(startId));
} else {
revWalk.markStart(revWalk.lookupCommit(head));
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
}
Iterator<RevCommit> iterator = revWalk.iterator();
@@ -234,10 +235,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
}
}
changesets = new ChangesetPagingResult(counter, changesetList);
if (branch != null) {
changesets = new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName()));
} else {
changesets = new ChangesetPagingResult(counter, changesetList);
}
} else if (logger.isWarnEnabled()) {
logger.warn("the repository {} seems to be empty",
repository.getName());
this.repository.getName());
changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST);
}

View File

@@ -0,0 +1,64 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.inject.util.Providers;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.web.JsonEnricherContext;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.MediaType;
import java.net.URI;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
public class GitConfigInIndexResourceTest {
@Rule
public final ShiroRule shiroRule = new ShiroRule();
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectNode root = objectMapper.createObjectNode();
private final GitConfigInIndexResource gitConfigInIndexResource;
public GitConfigInIndexResourceTest() {
root.put("_links", objectMapper.createObjectNode());
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
pathInfoStore.set(() -> URI.create("/"));
gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
}
@Test
@SubjectAware(username = "admin", password = "secret")
public void admin() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
gitConfigInIndexResource.enrich(context);
assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText());
}
@Test
@SubjectAware(username = "readOnly", password = "secret")
public void user() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
gitConfigInIndexResource.enrich(context);
assertFalse(root.get("_links").iterator().hasNext());
}
@Test
public void anonymous() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
gitConfigInIndexResource.enrich(context);
assertFalse(root.get("_links").iterator().hasNext());
}
}

View File

@@ -1,3 +1,4 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
@@ -33,14 +34,17 @@
package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.io.Files;
import org.junit.Test;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitConstants;
import sonia.scm.repository.Modifications;
import java.io.File;
import java.io.IOException;
import static java.nio.charset.Charset.defaultCharset;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -48,8 +52,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
//~--- JDK imports ------------------------------------------------------------
/**
* Unit tests for {@link GitLogCommand}.
*
@@ -72,6 +74,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId());
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId());
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId());
assertEquals("master", result.getBranchName());
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
// set default branch and fetch again
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
@@ -79,10 +83,12 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
result = createCommand().getChangesets(new LogCommandRequest());
assertNotNull(result);
assertEquals("test-branch", result.getBranchName());
assertEquals(3, result.getTotal());
assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getChangesets().get(0).getId());
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId());
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId());
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
}
@Test
@@ -210,6 +216,32 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
}
@Test
public void shouldFindDefaultBranchFromHEAD() throws Exception {
setRepositoryHeadReference("ref: refs/heads/test-branch");
ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
assertEquals("test-branch", changesets.getBranchName());
}
@Test
public void shouldFindMasterBranchWhenHEADisNoRef() throws Exception {
setRepositoryHeadReference("592d797cd36432e591416e8b2b98154f4f163411");
ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
assertEquals("master", changesets.getBranchName());
}
private void setRepositoryHeadReference(String s) throws IOException {
Files.write(s, repositoryHeadReferenceFile(), defaultCharset());
}
private File repositoryHeadReferenceFile() {
return new File(repositoryDirectory, "HEAD");
}
private GitLogCommand createCommand()
{
return new GitLogCommand(createContext(), repository);

View File

@@ -2,8 +2,10 @@
readOnly = secret, reader
writeOnly = secret, writer
readWrite = secret, readerWriter
admin = secret, admin
[roles]
reader = configuration:read:git
writer = configuration:write:git
readerWriter = configuration:*:git
admin = *

View File

@@ -0,0 +1,40 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.web.JsonEnricherBase;
import sonia.scm.web.JsonEnricherContext;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Collections.singletonMap;
import static sonia.scm.web.VndMediaType.INDEX;
@Extension
public class HgConfigInIndexResource extends JsonEnricherBase {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public HgConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
super(objectMapper);
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class)
.method("get")
.parameters()
.href();
JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl)));
addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode);
}
}
}

View File

@@ -132,7 +132,11 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
List<Changeset> changesets = on(repository).rev(start + ":"
+ end).execute();
result = new ChangesetPagingResult(total, changesets);
if (request.getBranch() == null) {
result = new ChangesetPagingResult(total, changesets);
} else {
result = new ChangesetPagingResult(total, changesets, request.getBranch());
}
}
else
{

View File

@@ -216,10 +216,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
String branch = in.textUpTo('\n');
if (!BRANCH_DEFAULT.equals(branch))
{
changeset.getBranches().add(branch);
}
changeset.getBranches().add(branch);
String p1 = readId(in, changeset, PROPERTY_PARENT1_REVISION);

View File

@@ -0,0 +1,64 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.inject.util.Providers;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.web.JsonEnricherContext;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.MediaType;
import java.net.URI;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
public class HgConfigInIndexResourceTest {
@Rule
public final ShiroRule shiroRule = new ShiroRule();
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectNode root = objectMapper.createObjectNode();
private final HgConfigInIndexResource hgConfigInIndexResource;
public HgConfigInIndexResourceTest() {
root.put("_links", objectMapper.createObjectNode());
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
pathInfoStore.set(() -> URI.create("/"));
hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
}
@Test
@SubjectAware(username = "admin", password = "secret")
public void admin() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
hgConfigInIndexResource.enrich(context);
assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText());
}
@Test
@SubjectAware(username = "readOnly", password = "secret")
public void user() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
hgConfigInIndexResource.enrich(context);
assertFalse(root.get("_links").iterator().hasNext());
}
@Test
public void anonymous() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
hgConfigInIndexResource.enrich(context);
assertFalse(root.get("_links").iterator().hasNext());
}
}

View File

@@ -88,6 +88,21 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
result.getChangesets().get(2).getId());
}
@Test
public void testGetDefaultBranchInfo() {
LogCommandRequest request = new LogCommandRequest();
request.setPath("a.txt");
ChangesetPagingResult result = createComamnd().getChangesets(request);
assertNotNull(result);
assertEquals(1,
result.getChangesets().get(0).getBranches().size());
assertEquals("default",
result.getChangesets().get(0).getBranches().get(0));
}
@Test
public void testGetAllWithLimit() {
LogCommandRequest request = new LogCommandRequest();

View File

@@ -2,8 +2,10 @@
readOnly = secret, reader
writeOnly = secret, writer
readWrite = secret, readerWriter
admin = secret, admin
[roles]
reader = configuration:read:hg
writer = configuration:write:hg
readerWriter = configuration:*:hg
admin = *

View File

@@ -0,0 +1,40 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.web.JsonEnricherBase;
import sonia.scm.web.JsonEnricherContext;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Collections.singletonMap;
import static sonia.scm.web.VndMediaType.INDEX;
@Extension
public class SvnConfigInIndexResource extends JsonEnricherBase {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public SvnConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
super(objectMapper);
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class)
.method("get")
.parameters()
.href();
JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl)));
addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode);
}
}
}

View File

@@ -32,122 +32,45 @@
package sonia.scm.web;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.filter.GZipFilter;
import sonia.scm.filter.GZipFilterConfig;
import sonia.scm.filter.GZipResponseWrapper;
import sonia.scm.repository.Repository;
import sonia.scm.repository.SvnRepositoryHandler;
import sonia.scm.repository.spi.ScmProviderHttpServlet;
import sonia.scm.util.WebUtil;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet
{
class SvnGZipFilter implements ScmProviderHttpServlet {
private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class);
private final SvnRepositoryHandler handler;
private final ScmProviderHttpServlet delegate;
//~--- constructors ---------------------------------------------------------
private GZipFilterConfig config = new GZipFilterConfig();
/**
* Constructs ...
*
*
* @param handler
*/
public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate)
{
SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) {
this.handler = handler;
this.delegate = delegate;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param filterConfig
*
* @throws ServletException
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
super.init(filterConfig);
getConfig().setBufferResponse(false);
}
/**
* Method description
*
*
* @param request
* @param response
* @param chain
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilter(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (handler.getConfig().isEnabledGZip())
{
if (logger.isTraceEnabled())
{
logger.trace("encode svn request with gzip");
}
super.doFilter(request, response, chain);
}
else
{
if (logger.isTraceEnabled())
{
logger.trace("skip gzip encoding");
}
chain.doFilter(request, response);
}
config.setBufferResponse(false);
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
if (handler.getConfig().isEnabledGZip())
{
if (logger.isTraceEnabled())
{
logger.trace("encode svn request with gzip");
}
super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository));
}
else
{
if (logger.isTraceEnabled())
{
logger.trace("skip gzip encoding");
}
if (handler.getConfig().isEnabledGZip() && WebUtil.isGzipSupported(request)) {
logger.trace("compress svn response with gzip");
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, config);
delegate.service(request, wrappedResponse, repository);
wrappedResponse.finishResponse();
} else {
logger.trace("skip gzip encoding");
delegate.service(request, response, repository);
}
}

View File

@@ -0,0 +1,64 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.inject.util.Providers;
import org.junit.Rule;
import org.junit.Test;
import sonia.scm.web.JsonEnricherContext;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.core.MediaType;
import java.net.URI;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
public class SvnConfigInIndexResourceTest {
@Rule
public final ShiroRule shiroRule = new ShiroRule();
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectNode root = objectMapper.createObjectNode();
private final SvnConfigInIndexResource svnConfigInIndexResource;
public SvnConfigInIndexResourceTest() {
root.put("_links", objectMapper.createObjectNode());
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
pathInfoStore.set(() -> URI.create("/"));
svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
}
@Test
@SubjectAware(username = "admin", password = "secret")
public void admin() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
svnConfigInIndexResource.enrich(context);
assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText());
}
@Test
@SubjectAware(username = "readOnly", password = "secret")
public void user() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
svnConfigInIndexResource.enrich(context);
assertFalse(root.get("_links").iterator().hasNext());
}
@Test
public void anonymous() {
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
svnConfigInIndexResource.enrich(context);
assertFalse(root.get("_links").iterator().hasNext());
}
}

View File

@@ -2,8 +2,10 @@
readOnly = secret, reader
writeOnly = secret, writer
readWrite = secret, readerWriter
admin = secret, admin
[roles]
reader = configuration:read:svn
writer = configuration:write:svn
readerWriter = configuration:*:svn
admin = *

View File

@@ -4,7 +4,8 @@
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap",
"link": "lerna exec -- yarn link"
"link": "lerna exec -- yarn link",
"unlink": "lerna exec --no-bail -- yarn unlink"
},
"devDependencies": {
"lerna": "^3.2.1"

View File

@@ -0,0 +1,39 @@
//@flow
import React from "react";
import injectSheet from "react-jss";
import classNames from "classnames";
const styles = {
img: {
display: "block"
},
q: {
float: "left",
paddingLeft: "3px",
float: "right"
}
};
type Props = {
message: string,
classes: any
};
class Help extends React.Component<Props> {
render() {
const { message, classes } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<div
className={classNames("tooltip is-tooltip-right", multiline, classes.q)}
data-tooltip={message}
>
<i
className={classNames("fa fa-question has-text-info", classes.img)}
/>
</div>
);
}
}
export default injectSheet(styles)(Help);

View File

@@ -0,0 +1,46 @@
//@flow
import React from "react";
import { Help } from "./index";
type Props = {
label: string,
helpText?: string
};
class LabelWithHelpIcon extends React.Component<Props> {
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
renderHelp = () => {
const helpText = this.props.helpText;
if (helpText) {
return (
<div className="control columns is-vcentered">
<Help message={helpText} />
</div>
);
} else return null;
};
renderLabelWithHelpIcon = () => {
if (this.props.label) {
return (
<div className="field is-grouped">
<div className="control">{this.renderLabel()}</div>
{this.renderHelp()}
</div>
);
} else return null;
};
render() {
return this.renderLabelWithHelpIcon();
}
}
export default LabelWithHelpIcon;

View File

@@ -32,7 +32,7 @@ export function createUrl(url: string) {
if (url.indexOf("/") !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash;
}
return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
return `${contextPath}/api/v2${urlWithStartingSlash}`;
}
class ApiClient {

View File

@@ -9,7 +9,7 @@ describe("create url", () => {
});
it("should add prefix for api", () => {
expect(createUrl("/users")).toBe("/api/rest/v2/users");
expect(createUrl("users")).toBe("/api/rest/v2/users");
expect(createUrl("/users")).toBe("/api/v2/users");
expect(createUrl("users")).toBe("/api/v2/users");
});
});

View File

@@ -9,7 +9,8 @@ type Props = {
disabled: boolean,
buttonLabel: string,
fieldLabel: string,
errorMessage: string
errorMessage: string,
helpText?: string
};
type State = {
@@ -25,7 +26,13 @@ class AddEntryToTableField extends React.Component<Props, State> {
}
render() {
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
const {
disabled,
buttonLabel,
fieldLabel,
errorMessage,
helpText
} = this.props;
return (
<div className="field">
<InputField
@@ -36,6 +43,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
value={this.state.entryToAdd}
onReturnPressed={this.appendEntry}
disabled={disabled}
helpText={helpText}
/>
<AddButton
label={buttonLabel}

View File

@@ -1,11 +1,13 @@
//@flow
import React from "react";
import { Help } from "../index";
type Props = {
label?: string,
checked: boolean,
onChange?: boolean => void,
disabled?: boolean
disabled?: boolean,
helpText?: string
};
class Checkbox extends React.Component<Props> {
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
@@ -14,9 +16,20 @@ class Checkbox extends React.Component<Props> {
}
};
renderHelp = () => {
const helpText = this.props.helpText;
if (helpText) {
return (
<div className="control columns is-vcentered">
<Help message={helpText} />
</div>
);
} else return null;
};
render() {
return (
<div className="field">
<div className="field is-grouped">
<div className="control">
<label className="checkbox" disabled={this.props.disabled}>
<input
@@ -28,6 +41,7 @@ class Checkbox extends React.Component<Props> {
{this.props.label}
</label>
</div>
{this.renderHelp()}
</div>
);
}

View File

@@ -1,6 +1,7 @@
//@flow
import React from "react";
import classNames from "classnames";
import { LabelWithHelpIcon } from "../index";
type Props = {
label?: string,
@@ -12,7 +13,8 @@ type Props = {
onReturnPressed?: () => void,
validationError: boolean,
errorMessage: string,
disabled?: boolean
disabled?: boolean,
helpText?: string
};
class InputField extends React.Component<Props> {
@@ -33,15 +35,6 @@ class InputField extends React.Component<Props> {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
const onReturnPressed = this.props.onReturnPressed;
if (!onReturnPressed) {
@@ -60,7 +53,9 @@ class InputField extends React.Component<Props> {
value,
validationError,
errorMessage,
disabled
disabled,
label,
helpText
} = this.props;
const errorView = validationError ? "is-danger" : "";
const helper = validationError ? (
@@ -70,7 +65,7 @@ class InputField extends React.Component<Props> {
);
return (
<div className="field">
{this.renderLabel()}
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<input
ref={input => {

View File

@@ -1,5 +1,7 @@
//@flow
import React from "react";
import classNames from "classnames";
import { LabelWithHelpIcon } from "../index";
export type SelectItem = {
value: string,
@@ -10,7 +12,9 @@ type Props = {
label?: string,
options: SelectItem[],
value?: SelectItem,
onChange: string => void
onChange: string => void,
loading?: boolean,
helpText?: string
};
class Select extends React.Component<Props> {
@@ -28,21 +32,18 @@ class Select extends React.Component<Props> {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { options, value } = this.props;
const { options, value, label, helpText, loading } = this.props;
const loadingClass = loading ? "is-loading" : "";
return (
<div className="field">
{this.renderLabel()}
<div className="control select">
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className={classNames(
"control select",
loadingClass
)}>
<select
ref={input => {
this.field = input;

View File

@@ -1,5 +1,6 @@
//@flow
import React from "react";
import { LabelWithHelpIcon } from "../index";
export type SelectItem = {
value: string,
@@ -10,7 +11,8 @@ type Props = {
label?: string,
placeholder?: SelectItem[],
value?: string,
onChange: string => void
onChange: string => void,
helpText?: string
};
class Textarea extends React.Component<Props> {
@@ -20,20 +22,12 @@ class Textarea extends React.Component<Props> {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { placeholder, value } = this.props;
const { placeholder, value, label, helpText } = this.props;
return (
<div className="field">
{this.renderLabel()}
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<textarea
className="textarea"

View File

@@ -16,11 +16,11 @@ export { default as MailLink } from "./MailLink.js";
export { default as Notification } from "./Notification.js";
export { default as Paginator } from "./Paginator.js";
export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
export * from "./buttons";
export * from "./forms";
export * from "./layout";

View File

@@ -1,5 +1,5 @@
// @flow
const nameRegex = /^([A-z0-9.\-_@]|[^ ]([A-z0-9.\-_@ ]*[A-z0-9.\-_@]|[^\s])?)$/;
const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/;
export const isNameValid = (name: string) => {
return nameRegex.test(name);

View File

@@ -5,6 +5,7 @@ describe("test name validation", () => {
it("should return false", () => {
// invalid names taken from ValidationUtilTest.java
const invalidNames = [
"@test",
" test 123",
" test 123 ",
"test 123 ",
@@ -35,10 +36,9 @@ describe("test name validation", () => {
"Test123-git",
"Test_user-123.git",
"test@scm-manager.de",
"test 123",
"test123",
"tt",
"t",
"valid_name",
"another1",
"stillValid",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
//@flow
import type { Links } from "./hal";
export type Permission = {
name: string,
type: string,
groupPermission: boolean,
_links?: Links
};
export type PermissionEntry = {
name: string,
type: string,
groupPermission: boolean
}
export type PermissionCollection = Permission[];

View File

@@ -10,3 +10,5 @@ export type { Repository, RepositoryCollection, RepositoryGroup } from "./Reposi
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
export type { Config } from "./Config";
export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions";

View File

@@ -48,6 +48,16 @@
<script>bootstrap</script>
</configuration>
</execution>
<execution>
<id>unlink</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<script>unlink</script>
</configuration>
</execution>
<execution>
<id>link</id>
<phase>package</phase>

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"@fortawesome/fontawesome-free": "^5.3.1",
"@scm-manager/ui-extensions": "^0.0.7",
"bulma": "^0.7.1",
"bulma-tooltip": "^2.0.2",
"classnames": "^2.2.5",
"font-awesome": "^4.7.0",
"history": "^4.7.2",
@@ -15,6 +16,7 @@
"i18next-browser-languagedetector": "^2.2.2",
"i18next-fetch-backend": "^0.1.0",
"moment": "^2.22.2",
"node-sass": "^4.9.3",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-i18next": "^7.9.0",

View File

@@ -64,5 +64,29 @@
"login-attempt-limit-timeout-invalid": "This is not a number",
"login-attempt-limit-invalid": "This is not a number",
"plugin-url-invalid": "This is not a valid url"
},
"help": {
"realmDescriptionHelpText": "Enter authentication realm description",
"dateFormatHelpText": "Moments date format. Please have a look at the momentjs documentation.",
"pluginRepositoryHelpText": "The url of the plugin repository. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
"enableForwardingHelpText": "Enbale mod_proxy port forwarding.",
"enableRepositoryArchiveHelpText": "Enable repository archives. A complete page reload is required after a change of this value.",
"disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.",
"allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.",
"skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.",
"adminGroupsHelpText": "Names of groups with admin permissions.",
"adminUsersHelpText": "Names of users with admin permissions.",
"forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url",
"baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm",
"loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.",
"loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.",
"enableProxyHelpText": "Enable Proxy",
"proxyPortHelpText": "The proxy port",
"proxyPasswordHelpText": "The password for the proxy server authentication.",
"proxyServerHelpText": "The proxy server",
"proxyUserHelpText": "The username for the proxy server authentication.",
"proxyExcludesHelpText": "Glob patterns for hostnames which should be excluded from proxy settings.",
"enableXsrfProtectionHelpText": "Enable Xsrf Cookie Protection. Note: This feature is still experimental.",
"defaultNameSpaceStrategyHelpText": "The default namespace strategy"
}
}

View File

@@ -42,7 +42,12 @@
"group-form": {
"submit": "Submit",
"name-error": "Group name is invalid",
"description-error": "Description is invalid"
"description-error": "Description is invalid",
"help": {
"nameHelpText": "Unique name of the group",
"descriptionHelpText": "A short description of the group",
"memberHelpText": "Usernames of the group members"
}
},
"delete-group-button": {
"label": "Delete",

View File

@@ -22,7 +22,8 @@
"actions-label": "Actions",
"back-label": "Back",
"navigation-label": "Navigation",
"information": "Information"
"information": "Information",
"permissions": "Permissions"
},
"create": {
"title": "Create Repository",
@@ -42,5 +43,36 @@
"submit": "Yes",
"cancel": "No"
}
},
"permission": {
"error-title": "Error",
"error-subtitle": "Unknown permissions error",
"name": "User or Group",
"type": "Type",
"group-permission": "Group Permission",
"edit-permission": {
"delete-button": "Delete",
"save-button": "Save Changes"
},
"delete-permission-button": {
"label": "Delete",
"confirm-alert": {
"title": "Delete permission",
"message": "Do you really want to delete the permission?",
"submit": "Yes",
"cancel": "No"
}
},
"add-permission": {
"add-permission-heading": "Add new Permission",
"submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
}
},
"help": {
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
"contactHelpText": "Email address of the person who is responsible for this repository.",
"descriptionHelpText": "A short description of the repository."
}
}

View File

@@ -51,5 +51,14 @@
"password-invalid": "Password has to be between 6 and 32 characters",
"passwordValidation-invalid": "Passwords have to be the same",
"validatePassword": "Please validate password here"
},
"help": {
"usernameHelpText": "Unique name of the user.",
"displayNameHelpText": "Display name of the user.",
"mailHelpText": "Email address of the user.",
"passwordHelpText": "Plain text password of the user.",
"passwordConfirmHelpText": "Repeat the password for validation.",
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
"activeHelpText": "Activate or deactive the user."
}
}

View File

@@ -23,12 +23,14 @@ class BaseUrlSettings extends React.Component<Props> {
label={t("base-url-settings.force-base-url")}
onChange={this.handleForceBaseUrlChange}
disabled={!hasUpdatePermission}
helpText={t("help.forceBaseUrlHelpText")}
/>
<InputField
label={t("base-url-settings.base-url")}
onChange={this.handleBaseUrlChange}
value={baseUrl}
disabled={!hasUpdatePermission}
helpText={t("help.baseUrlHelpText")}
/>
</div>
);

View File

@@ -41,54 +41,63 @@ class GeneralSettings extends React.Component<Props> {
onChange={this.handleRealmDescriptionChange}
value={realmDescription}
disabled={!hasUpdatePermission}
helpText={t("help.realmDescriptionHelpText")}
/>
<InputField
label={t("general-settings.date-format")}
onChange={this.handleDateFormatChange}
value={dateFormat}
disabled={!hasUpdatePermission}
helpText={t("help.dateFormatHelpText")}
/>
<InputField
label={t("general-settings.plugin-url")}
onChange={this.handlePluginUrlChange}
value={pluginUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginRepositoryHelpText")}
/>
<InputField
label={t("general-settings.default-namespace-strategy")}
onChange={this.handleDefaultNamespaceStrategyChange}
value={defaultNamespaceStrategy}
disabled={!hasUpdatePermission}
helpText={t("help.defaultNameSpaceStrategyHelpText")}
/>
<Checkbox
checked={enabledXsrfProtection}
label={t("general-settings.enabled-xsrf-protection")}
onChange={this.handleEnabledXsrfProtectionChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableXsrfProtectionHelpText")}
/>
<Checkbox
checked={enableRepositoryArchive}
label={t("general-settings.enable-repository-archive")}
onChange={this.handleEnableRepositoryArchiveChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableRepositoryArchiveHelpText")}
/>
<Checkbox
checked={disableGroupingGrid}
label={t("general-settings.disable-grouping-grid")}
onChange={this.handleDisableGroupingGridChange}
disabled={!hasUpdatePermission}
helpText={t("help.disableGroupingGridHelpText")}
/>
<Checkbox
checked={anonymousAccessEnabled}
label={t("general-settings.anonymous-access-enabled")}
onChange={this.handleAnonymousAccessEnabledChange}
disabled={!hasUpdatePermission}
helpText={t("help.allowAnonymousAccessHelpText")}
/>
<Checkbox
checked={skipFailedAuthenticators}
label={t("general-settings.skip-failed-authenticators")}
onChange={this.handleSkipFailedAuthenticatorsChange}
disabled={!hasUpdatePermission}
helpText={t("help.skipFailedAuthenticatorsHelpText")}
/>
</div>
);

View File

@@ -47,6 +47,7 @@ class LoginAttempt extends React.Component<Props, State> {
disabled={!hasUpdatePermission}
validationError={this.state.loginAttemptLimitError}
errorMessage={t("validation.login-attempt-limit-invalid")}
helpText={t("help.loginAttemptLimitHelpText")}
/>
<InputField
label={t("login-attempt.login-attempt-limit-timeout")}
@@ -55,6 +56,7 @@ class LoginAttempt extends React.Component<Props, State> {
disabled={!hasUpdatePermission}
validationError={this.state.loginAttemptLimitTimeoutError}
errorMessage={t("validation.login-attempt-limit-timeout-invalid")}
helpText={t("help.loginAttemptLimitTimeoutHelpText")}
/>
</div>
);

View File

@@ -42,6 +42,7 @@ class ProxySettings extends React.Component<Props> {
label={t("proxy-settings.enable-proxy")}
onChange={this.handleEnableProxyChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableProxyHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-password")}
@@ -49,24 +50,28 @@ class ProxySettings extends React.Component<Props> {
value={proxyPassword}
type="password"
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyPasswordHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-port")}
value={proxyPort}
onChange={this.handleProxyPortChange}
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyPortHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-server")}
value={proxyServer}
onChange={this.handleProxyServerChange}
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyServerHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-user")}
value={proxyUser}
onChange={this.handleProxyUserChange}
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyUserHelpText")}
/>
<ProxyExcludesTable
proxyExcludes={proxyExcludes}

View File

@@ -24,6 +24,7 @@ class AdminGroupTable extends React.Component<Props, State> {
removeLabel={t("admin-settings.remove-group-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.adminGroupsHelpText")}
/>
);
}

View File

@@ -22,6 +22,7 @@ class AdminUserTable extends React.Component<Props> {
removeLabel={t("admin-settings.remove-user-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.adminUsersHelpText")}
/>
);
}

View File

@@ -1,21 +1,22 @@
//@flow
import React from "react";
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
import { RemoveEntryOfTableButton, LabelWithHelpIcon } from "@scm-manager/ui-components";
type Props = {
items: string[],
label: string,
removeLabel: string,
onRemove: (string[], string) => void,
disabled: boolean
disabled: boolean,
helpText: string
};
class ArrayConfigTable extends React.Component<Props> {
render() {
const { label, disabled, removeLabel, items } = this.props;
const { label, disabled, removeLabel, items, helpText } = this.props;
return (
<div>
<label className="label">{label}</label>
<LabelWithHelpIcon label={label} helpText={helpText}/>
<table className="table is-hoverable is-fullwidth">
<tbody>
{items.map(item => {

View File

@@ -22,6 +22,7 @@ class ProxyExcludesTable extends React.Component<Props, State> {
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.proxyExcludesHelpText")}
/>
);
}

View File

@@ -23,7 +23,7 @@ import reducer, {
getConfigUpdatePermission
} from "./config";
const CONFIG_URL = "/api/rest/v2/config";
const CONFIG_URL = "/api/v2/config";
const error = new Error("You have an error!");
@@ -51,8 +51,8 @@ const config = {
enabledXsrfProtection: true,
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
_links: {
self: { href: "http://localhost:8081/api/rest/v2/config" },
update: { href: "http://localhost:8081/api/rest/v2/config" }
self: { href: "http://localhost:8081/api/v2/config" },
update: { href: "http://localhost:8081/api/v2/config" }
}
};
@@ -80,8 +80,8 @@ const configWithNullValues = {
enabledXsrfProtection: true,
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
_links: {
self: { href: "http://localhost:8081/api/rest/v2/config" },
update: { href: "http://localhost:8081/api/rest/v2/config" }
self: { href: "http://localhost:8081/api/v2/config" },
update: { href: "http://localhost:8081/api/v2/config" }
}
};
@@ -135,7 +135,7 @@ describe("config fetch()", () => {
});
it("should successfully modify config", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
status: 204
});
@@ -150,7 +150,7 @@ describe("config fetch()", () => {
});
it("should call the callback after modifying config", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
status: 204
});
@@ -169,7 +169,7 @@ describe("config fetch()", () => {
});
it("should fail modifying config on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
status: 500
});

View File

@@ -11,6 +11,7 @@ import groups from "./groups/modules/groups";
import auth from "./modules/auth";
import pending from "./modules/pending";
import failure from "./modules/failure";
import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config";
import type { BrowserHistory } from "history/createBrowserHistory";
@@ -26,6 +27,7 @@ function createReduxStore(history: BrowserHistory) {
users,
repos,
repositoryTypes,
permissions,
groups,
auth,
config

View File

@@ -80,6 +80,7 @@ class GroupForm extends React.Component<Props, State> {
onChange={this.handleGroupNameChange}
value={group.name}
validationError={this.state.nameValidationError}
helpText={t("group-form.help.nameHelpText")}
/>
);
}
@@ -93,6 +94,7 @@ class GroupForm extends React.Component<Props, State> {
onChange={this.handleDescriptionChange}
value={group.description}
validationError={false}
helpText={t("group-form.help.descriptionHelpText")}
/>
<MemberNameTable
members={this.state.group.members}

View File

@@ -1,7 +1,10 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
import {
RemoveEntryOfTableButton,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
type Props = {
members: string[],
@@ -16,7 +19,10 @@ class MemberNameTable extends React.Component<Props, State> {
const { t } = this.props;
return (
<div>
<label className="label">{t("group.members")}</label>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("group-form.help.memberHelpText")}
/>
<table className="table is-hoverable is-fullwidth">
<tbody>
{this.props.members.map(member => {

View File

@@ -44,7 +44,7 @@ import reducer, {
MODIFY_GROUP_SUCCESS,
MODIFY_GROUP_FAILURE
} from "./groups";
const GROUPS_URL = "/api/rest/v2/groups";
const GROUPS_URL = "/api/v2/groups";
const error = new Error("You have an error!");
@@ -57,13 +57,13 @@ const humanGroup = {
members: ["userZaphod"],
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
href: "http://localhost:8081/api/v2/groups/humanGroup"
},
delete: {
href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
href: "http://localhost:8081/api/v2/groups/humanGroup"
},
update: {
href:"http://localhost:8081/api/rest/v2/groups/humanGroup"
href:"http://localhost:8081/api/v2/groups/humanGroup"
}
},
_embedded: {
@@ -72,7 +72,7 @@ const humanGroup = {
name: "userZaphod",
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/users/userZaphod"
href: "http://localhost:8081/api/v2/users/userZaphod"
}
}
}
@@ -89,13 +89,13 @@ const emptyGroup = {
members: [],
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
href: "http://localhost:8081/api/v2/groups/emptyGroup"
},
delete: {
href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
href: "http://localhost:8081/api/v2/groups/emptyGroup"
},
update: {
href:"http://localhost:8081/api/rest/v2/groups/emptyGroup"
href:"http://localhost:8081/api/v2/groups/emptyGroup"
}
},
_embedded: {
@@ -108,16 +108,16 @@ const responseBody = {
pageTotal: 1,
_links: {
self: {
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
},
first: {
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
},
last: {
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
},
create: {
href: "http://localhost:3000/api/rest/v2/groups/"
href: "http://localhost:3000/api/v2/groups/"
}
},
_embedded: {
@@ -244,7 +244,7 @@ describe("groups fetch()", () => {
});
it("should successfully modify group", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -259,7 +259,7 @@ describe("groups fetch()", () => {
});
it("should call the callback after modifying group", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -278,7 +278,7 @@ describe("groups fetch()", () => {
});
it("should fail modifying group on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 500
});
@@ -293,7 +293,7 @@ describe("groups fetch()", () => {
});
it("should delete successfully group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -308,7 +308,7 @@ describe("groups fetch()", () => {
});
it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -324,7 +324,7 @@ describe("groups fetch()", () => {
});
it("should fail to delete group humanGroup", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 500
});

View File

@@ -78,7 +78,7 @@ describe("auth actions", () => {
});
it("should dispatch login success and dispatch fetch me", () => {
fetchMock.postOnce("/api/rest/v2/auth/access_token", {
fetchMock.postOnce("/api/v2/auth/access_token", {
body: {
cookie: true,
grant_type: "password",
@@ -88,7 +88,7 @@ describe("auth actions", () => {
headers: { "content-type": "application/json" }
});
fetchMock.getOnce("/api/rest/v2/me", {
fetchMock.getOnce("/api/v2/me", {
body: me,
headers: { "content-type": "application/json" }
});
@@ -106,7 +106,7 @@ describe("auth actions", () => {
});
it("should dispatch login failure", () => {
fetchMock.postOnce("/api/rest/v2/auth/access_token", {
fetchMock.postOnce("/api/v2/auth/access_token", {
status: 400
});
@@ -120,7 +120,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me success", () => {
fetchMock.getOnce("/api/rest/v2/me", {
fetchMock.getOnce("/api/v2/me", {
body: me,
headers: { "content-type": "application/json" }
});
@@ -141,7 +141,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me failure", () => {
fetchMock.getOnce("/api/rest/v2/me", {
fetchMock.getOnce("/api/v2/me", {
status: 500
});
@@ -155,7 +155,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me unauthorized", () => {
fetchMock.getOnce("/api/rest/v2/me", {
fetchMock.getOnce("/api/v2/me", {
status: 401
});
@@ -173,11 +173,11 @@ describe("auth actions", () => {
});
it("should dispatch logout success", () => {
fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
fetchMock.deleteOnce("/api/v2/auth/access_token", {
status: 204
});
fetchMock.getOnce("/api/rest/v2/me", {
fetchMock.getOnce("/api/v2/me", {
status: 401
});
@@ -194,7 +194,7 @@ describe("auth actions", () => {
});
it("should dispatch logout failure", () => {
fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
fetchMock.deleteOnce("/api/v2/auth/access_token", {
status: 500
});

View File

@@ -13,6 +13,20 @@ function extractIdentifierFromFailure(action: Action) {
return identifier;
}
function removeAllEntriesOfIdentifierFromState(
state: Object,
payload: any,
identifier: string
) {
const newState = {};
for (let failureType in state) {
if (failureType !== identifier && !failureType.startsWith(identifier)) {
newState[failureType] = state[failureType];
}
}
return newState;
}
function removeFromState(state: Object, identifier: string) {
const newState = {};
for (let failureType in state) {
@@ -47,7 +61,9 @@ export default function reducer(
if (action.itemId) {
identifier += "/" + action.itemId;
}
return removeFromState(state, identifier);
if (action.payload)
return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
else return removeFromState(state, identifier);
}
}
return state;

View File

@@ -19,6 +19,20 @@ function removeFromState(state: Object, identifier: string) {
return newState;
}
function removeAllEntriesOfIdentifierFromState(
state: Object,
payload: any,
identifier: string
) {
const newState = {};
for (let childType in state) {
if (childType !== identifier && !childType.startsWith(identifier)) {
newState[childType] = state[childType];
}
}
return newState;
}
function extractIdentifierFromPending(action: Action) {
const type = action.type;
let identifier = type.substring(0, type.length - PENDING_SUFFIX.length);
@@ -48,7 +62,10 @@ export default function reducer(
if (action.itemId) {
identifier += "/" + action.itemId;
}
return removeFromState(state, identifier);
if (action.payload)
return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
else
return removeFromState(state, identifier);
}
}
}

View File

@@ -0,0 +1,28 @@
//@flow
import React from "react";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Repository } from "@scm-manager/ui-types";
type Props = {
permissionUrl: string,
t: string => string,
repository: Repository
};
class PermissionsNavLink extends React.Component<Props> {
hasPermissionsLink = () => {
return this.props.repository._links.permissions;
};
render() {
if (!this.hasPermissionsLink()) {
return null;
}
const { permissionUrl, t } = this.props;
return (
<NavLink to={permissionUrl} label={t("repository-root.permissions")} />
);
}
}
export default translate("repos")(PermissionsNavLink);

View File

@@ -0,0 +1,39 @@
import React from "react";
import { mount, shallow } from "enzyme";
import "../../tests/enzyme";
import "../../tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import PermissionsNavLink from "./PermissionsNavLink";
import EditNavLink from "./EditNavLink";
describe("PermissionsNavLink", () => {
const options = new ReactRouterEnzymeContext();
it("should render nothing, if the modify link is missing", () => {
const repository = {
_links: {}
};
const navLink = shallow(
<PermissionsNavLink repository={repository} permissionUrl="" />,
options.get()
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const repository = {
_links: {
permissions: {
href: "/permissions"
}
}
};
const navLink = mount(
<PermissionsNavLink repository={repository} permissionUrl="" />,
options.get()
);
expect(navLink.text()).toBe("repository-root.permissions");
});
});

View File

@@ -90,12 +90,14 @@ class RepositoryForm extends React.Component<Props, State> {
value={repository ? repository.contact : ""}
validationError={this.state.contactValidationError}
errorMessage={t("validation.contact-invalid")}
helpText={t("help.contactHelpText")}
/>
<Textarea
label={t("repository.description")}
onChange={this.handleDescriptionChange}
value={repository ? repository.description : ""}
helpText={t("help.descriptionHelpText")}
/>
<SubmitButton
disabled={!this.isValid()}
@@ -129,12 +131,14 @@ class RepositoryForm extends React.Component<Props, State> {
value={repository ? repository.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
helpText={t("help.nameHelpText")}
/>
<Select
label={t("repository.type")}
onChange={this.handleTypeChange}
value={repository ? repository.type : ""}
options={this.createSelectOptions(repositoryTypes)}
helpText={t("help.typeHelpText")}
/>
</div>
);

View File

@@ -22,9 +22,11 @@ import { translate } from "react-i18next";
import RepositoryDetails from "../components/RepositoryDetails";
import DeleteNavAction from "../components/DeleteNavAction";
import Edit from "../containers/Edit";
import Permissions from "../permissions/containers/Permissions";
import type { History } from "history";
import EditNavLink from "../components/EditNavLink";
import PermissionsNavLink from "../components/PermissionsNavLink";
type Props = {
namespace: string,
@@ -101,11 +103,24 @@ class RepositoryRoot extends React.Component<Props> {
path={`${url}/edit`}
component={() => <Edit repository={repository} />}
/>
<Route
path={`${url}/permissions`}
render={props => (
<Permissions
namespace={this.props.repository.namespace}
repoName={this.props.repository.name}
/>
)}
/>
</div>
<div className="column">
<Navigation>
<Section label={t("repository-root.navigation-label")}>
<NavLink to={url} label={t("repository-root.information")} />
<PermissionsNavLink
permissionUrl={`${url}/permissions`}
repository={repository}
/>
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
</Section>
<Section label={t("repository-root.actions-label")}>

View File

@@ -58,33 +58,33 @@ const hitchhikerPuzzle42: Repository = {
type: "svn",
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42"
href: "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42"
},
delete: {
href: "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42"
href: "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42"
},
update: {
href: "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42"
href: "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42"
},
permissions: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/"
"http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/permissions/"
},
tags: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/tags/"
"http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/tags/"
},
branches: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/branches/"
"http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/branches/"
},
changesets: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/changesets/"
"http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/changesets/"
},
sources: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/sources/"
"http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/sources/"
}
}
};
@@ -100,35 +100,35 @@ const hitchhikerRestatend: Repository = {
_links: {
self: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
},
delete: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
},
update: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
},
permissions: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/permissions/"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend/permissions/"
},
tags: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/tags/"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend/tags/"
},
branches: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/branches/"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend/branches/"
},
changesets: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/changesets/"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend/changesets/"
},
sources: {
href:
"http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/sources/"
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend/sources/"
}
}
};
@@ -142,32 +142,32 @@ const slartiFjords: Repository = {
creationDate: "2018-07-31T08:59:05.653Z",
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords"
href: "http://localhost:8081/api/v2/repositories/slarti/fjords"
},
delete: {
href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords"
href: "http://localhost:8081/api/v2/repositories/slarti/fjords"
},
update: {
href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords"
href: "http://localhost:8081/api/v2/repositories/slarti/fjords"
},
permissions: {
href:
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords/permissions/"
"http://localhost:8081/api/v2/repositories/slarti/fjords/permissions/"
},
tags: {
href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords/tags/"
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/tags/"
},
branches: {
href:
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords/branches/"
"http://localhost:8081/api/v2/repositories/slarti/fjords/branches/"
},
changesets: {
href:
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords/changesets/"
"http://localhost:8081/api/v2/repositories/slarti/fjords/changesets/"
},
sources: {
href:
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords/sources/"
"http://localhost:8081/api/v2/repositories/slarti/fjords/sources/"
}
}
};
@@ -177,16 +177,16 @@ const repositoryCollection: RepositoryCollection = {
pageTotal: 1,
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
first: {
href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
last: {
href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
create: {
href: "http://localhost:8081/api/rest/v2/repositories/"
href: "http://localhost:8081/api/v2/repositories/"
}
},
_embedded: {
@@ -199,16 +199,16 @@ const repositoryCollectionWithNames: RepositoryCollection = {
pageTotal: 1,
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
first: {
href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
last: {
href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
create: {
href: "http://localhost:8081/api/rest/v2/repositories/"
href: "http://localhost:8081/api/v2/repositories/"
}
},
_embedded: {
@@ -221,7 +221,7 @@ const repositoryCollectionWithNames: RepositoryCollection = {
};
describe("repos fetch", () => {
const REPOS_URL = "/api/rest/v2/repositories";
const REPOS_URL = "/api/v2/repositories";
const SORT = "sortBy=namespaceAndName";
const REPOS_URL_WITH_SORT = REPOS_URL + "?" + SORT;
const mockStore = configureMockStore([thunk]);
@@ -293,7 +293,7 @@ describe("repos fetch", () => {
it("should append sortby parameter and successfully fetch repos from link", () => {
fetchMock.getOnce(
"/api/rest/v2/repositories?one=1&sortBy=namespaceAndName",
"/api/v2/repositories?one=1&sortBy=namespaceAndName",
repositoryCollection
);
@@ -421,7 +421,7 @@ describe("repos fetch", () => {
it("should successfully delete repo slarti/fjords", () => {
fetchMock.delete(
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords",
"http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 204
}
@@ -448,7 +448,7 @@ describe("repos fetch", () => {
it("should successfully delete repo slarti/fjords and call the callback", () => {
fetchMock.delete(
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords",
"http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 204
}
@@ -468,7 +468,7 @@ describe("repos fetch", () => {
it("should disapatch failure on delete, if server returns status code 500", () => {
fetchMock.delete(
"http://localhost:8081/api/rest/v2/repositories/slarti/fjords",
"http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 500
}

View File

@@ -22,7 +22,7 @@ const git = {
displayName: "Git",
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositoryTypes/git"
href: "http://localhost:8081/api/v2/repositoryTypes/git"
}
}
};
@@ -32,7 +32,7 @@ const hg = {
displayName: "Mercurial",
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositoryTypes/hg"
href: "http://localhost:8081/api/v2/repositoryTypes/hg"
}
}
};
@@ -42,7 +42,7 @@ const svn = {
displayName: "Subversion",
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositoryTypes/svn"
href: "http://localhost:8081/api/v2/repositoryTypes/svn"
}
}
};
@@ -53,7 +53,7 @@ const collection = {
},
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/repositoryTypes"
href: "http://localhost:8081/api/v2/repositoryTypes"
}
}
};
@@ -97,7 +97,7 @@ describe("repository types caching", () => {
});
describe("repository types fetch", () => {
const URL = "/api/rest/v2/repositoryTypes";
const URL = "/api/v2/repositoryTypes";
const mockStore = configureMockStore([thunk]);
afterEach(() => {

View File

@@ -0,0 +1,122 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField, SubmitButton } from "@scm-manager/ui-components";
import TypeSelector from "./TypeSelector";
import type {
PermissionCollection,
PermissionEntry
} from "@scm-manager/ui-types";
import * as validator from "./permissionValidation";
type Props = {
t: string => string,
createPermission: (permission: PermissionEntry) => void,
loading: boolean,
currentPermissions: PermissionCollection
};
type State = {
name: string,
type: string,
groupPermission: boolean,
valid: boolean
};
class CreatePermissionForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
name: "",
type: "READ",
groupPermission: false,
valid: true
};
}
render() {
const { t, loading } = this.props;
const { name, type, groupPermission } = this.state;
return (
<div>
<h2 className="subtitle">
{t("permission.add-permission.add-permission-heading")}
</h2>
<form onSubmit={this.submit}>
<InputField
label={t("permission.name")}
value={name ? name : ""}
onChange={this.handleNameChange}
validationError={!this.state.valid}
errorMessage={t("permission.add-permission.name-input-invalid")}
/>
<Checkbox
label={t("permission.group-permission")}
checked={groupPermission ? groupPermission : false}
onChange={this.handleGroupPermissionChange}
/>
<TypeSelector
label={t("permission.type")}
handleTypeChange={this.handleTypeChange}
type={type ? type : "READ"}
/>
<SubmitButton
label={t("permission.add-permission.submit-button")}
loading={loading}
disabled={!this.state.valid || this.state.name == ""}
/>
</form>
</div>
);
}
submit = e => {
this.props.createPermission({
name: this.state.name,
type: this.state.type,
groupPermission: this.state.groupPermission
});
this.removeState();
e.preventDefault();
};
removeState = () => {
this.setState({
name: "",
type: "READ",
groupPermission: false,
valid: true
});
};
handleTypeChange = (type: string) => {
this.setState({
type: type
});
};
handleNameChange = (name: string) => {
this.setState({
name: name,
valid: validator.isPermissionValid(
name,
this.state.groupPermission,
this.props.currentPermissions
)
});
};
handleGroupPermissionChange = (groupPermission: boolean) => {
this.setState({
groupPermission: groupPermission,
valid: validator.isPermissionValid(
this.state.name,
groupPermission,
this.props.currentPermissions
)
});
};
}
export default translate("repos")(CreatePermissionForm);

View File

@@ -0,0 +1,40 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import {
Select
} from "@scm-manager/ui-components";
type Props = {
t: string => string,
handleTypeChange: string => void,
type: string,
loading?: boolean
};
class TypeSelector extends React.Component<Props> {
render() {
const { type, handleTypeChange, loading } = this.props;
const types = ["READ", "OWNER", "WRITE"];
return (
<Select
onChange={handleTypeChange}
value={type ? type : "READ"}
options={this.createSelectOptions(types)}
loading={loading}
/>
);
}
createSelectOptions(types: string[]) {
return types.map(type => {
return {
label: type,
value: type
};
});
}
}
export default translate("permissions")(TypeSelector);

View File

@@ -0,0 +1,73 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { Permission } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton } from "@scm-manager/ui-components";
type Props = {
permission: Permission,
namespace: string,
repoName: string,
confirmDialog?: boolean,
t: string => string,
deletePermission: (
permission: Permission,
namespace: string,
repoName: string
) => void,
loading: boolean
};
class DeletePermissionButton extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
deletePermission = () => {
this.props.deletePermission(
this.props.permission,
this.props.namespace,
this.props.repoName
);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("permission.delete-permission-button.confirm-alert.title"),
message: t("permission.delete-permission-button.confirm-alert.message"),
buttons: [
{
label: t("permission.delete-permission-button.confirm-alert.submit"),
onClick: () => this.deletePermission()
},
{
label: t("permission.delete-permission-button.confirm-alert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.permission._links.delete;
};
render() {
const { confirmDialog, loading, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deletePermission;
if (!this.isDeletable()) {
return null;
}
return (
<DeleteButton
label={t("permission.delete-permission-button.label")}
action={action}
loading={loading}
/>
);
}
}
export default translate("repos")(DeletePermissionButton);

View File

@@ -0,0 +1,91 @@
import React from "react";
import { mount, shallow } from "enzyme";
import "../../../../tests/enzyme";
import "../../../../tests/i18n";
import DeletePermissionButton from "./DeletePermissionButton";
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
}));
describe("DeletePermissionButton", () => {
it("should render nothing, if the delete link is missing", () => {
const permission = {
_links: {}
};
const navLink = shallow(
<DeletePermissionButton
permission={permission}
deletePermission={() => {}}
/>
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const permission = {
_links: {
delete: {
href: "/permission"
}
}
};
const navLink = mount(
<DeletePermissionButton
permission={permission}
deletePermission={() => {}}
/>
);
expect(navLink.text()).not.toBe("");
});
it("should open the confirm dialog on button click", () => {
const permission = {
_links: {
delete: {
href: "/permission"
}
}
};
const button = mount(
<DeletePermissionButton
permission={permission}
deletePermission={() => {}}
/>
);
button.find("button").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
});
it("should call the delete permission function with delete url", () => {
const permission = {
_links: {
delete: {
href: "/permission"
}
}
};
let calledUrl = null;
function capture(permission) {
calledUrl = permission._links.delete.href;
}
const button = mount(
<DeletePermissionButton
permission={permission}
confirmDialog={false}
deletePermission={capture}
/>
);
button.find("button").simulate("click");
expect(calledUrl).toBe("/permission");
});
});

View File

@@ -0,0 +1,23 @@
// @flow
import { validation } from "@scm-manager/ui-components";
import type {
PermissionCollection,
} from "@scm-manager/ui-types";
const isNameValid = validation.isNameValid;
export { isNameValid };
export const isPermissionValid = (name: string, groupPermission: boolean, permissions: PermissionCollection) => {
return isNameValid(name) && !currentPermissionIncludeName(name, groupPermission, permissions);
};
const currentPermissionIncludeName = (name: string, groupPermission: boolean, permissions: PermissionCollection) => {
for (let i = 0; i < permissions.length; i++) {
if (
permissions[i].name === name &&
permissions[i].groupPermission == groupPermission
)
return true;
}
return false;
};

View File

@@ -0,0 +1,66 @@
//@flow
import * as validator from "./permissionValidation";
describe("permission validation", () => {
it("should return true if permission is valid and does not exist", () => {
const permissions = [];
const name = "PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(true);
});
it("should return true if permission is valid and does not exists with same group permission", () => {
const permissions = [
{
name: "PermissionName",
groupPermission: true,
type: "READ"
}
];
const name = "PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(true);
});
it("should return false if permission is valid but exists", () => {
const permissions = [
{
name: "PermissionName",
groupPermission: false,
type: "READ"
}
];
const name = "PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(false);
});
it("should return false if permission does not exist but is invalid", () => {
const permissions = [];
const name = "@PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(false);
});
it("should return false if permission is not valid and does not exist", () => {
const permissions = [];
const name = "@PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(false);
});
});

View File

@@ -0,0 +1,200 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import {
fetchPermissions,
getFetchPermissionsFailure,
isFetchPermissionsPending,
getPermissionsOfRepo,
hasCreatePermission,
createPermission,
isCreatePermissionPending,
getCreatePermissionFailure,
createPermissionReset,
getDeletePermissionsFailure,
getModifyPermissionsFailure,
modifyPermissionReset,
deletePermissionReset
} from "../modules/permissions";
import { Loading, ErrorPage } from "@scm-manager/ui-components";
import type {
Permission,
PermissionCollection,
PermissionEntry
} from "@scm-manager/ui-types";
import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "../components/CreatePermissionForm";
import type { History } from "history";
type Props = {
namespace: string,
repoName: string,
loading: boolean,
error: Error,
permissions: PermissionCollection,
hasPermissionToCreate: boolean,
loadingCreatePermission: boolean,
//dispatch functions
fetchPermissions: (namespace: string, repoName: string) => void,
createPermission: (
permission: PermissionEntry,
namespace: string,
repoName: string,
callback?: () => void
) => void,
createPermissionReset: (string, string) => void,
modifyPermissionReset: (string, string) => void,
deletePermissionReset: (string, string) => void,
// context props
t: string => string,
match: any,
history: History
};
class Permissions extends React.Component<Props> {
componentDidMount() {
const {
fetchPermissions,
namespace,
repoName,
modifyPermissionReset,
createPermissionReset,
deletePermissionReset
} = this.props;
createPermissionReset(namespace, repoName);
modifyPermissionReset(namespace, repoName);
deletePermissionReset(namespace, repoName);
fetchPermissions(namespace, repoName);
}
createPermission = (permission: Permission) => {
this.props.createPermission(
permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const {
loading,
error,
permissions,
t,
namespace,
repoName,
loadingCreatePermission,
hasPermissionToCreate
} = this.props;
if (error) {
return (
<ErrorPage
title={t("permission.error-title")}
subtitle={t("permission.error-subtitle")}
error={error}
/>
);
}
if (loading || !permissions) {
return <Loading />;
}
const createPermissionForm = hasPermissionToCreate ? (
<CreatePermissionForm
createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission}
currentPermissions={permissions}
/>
) : null;
return (
<div>
<table className="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("permission.name")}</th>
<th className="is-hidden-mobile">
{t("permission.group-permission")}
</th>
<th>{t("permission.type")}</th>
</tr>
</thead>
<tbody>
{permissions.map(permission => {
return (
<SinglePermission
key={permission.name + permission.groupPermission.toString()}
namespace={namespace}
repoName={repoName}
permission={permission}
/>
);
})}
</tbody>
</table>
{createPermissionForm}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const namespace = ownProps.namespace;
const repoName = ownProps.repoName;
const error =
getFetchPermissionsFailure(state, namespace, repoName) ||
getCreatePermissionFailure(state, namespace, repoName) ||
getDeletePermissionsFailure(state, namespace, repoName) ||
getModifyPermissionsFailure(state, namespace, repoName);
const loading = isFetchPermissionsPending(state, namespace, repoName);
const permissions = getPermissionsOfRepo(state, namespace, repoName);
const loadingCreatePermission = isCreatePermissionPending(
state,
namespace,
repoName
);
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
return {
namespace,
repoName,
error,
loading,
permissions,
hasPermissionToCreate,
loadingCreatePermission
};
};
const mapDispatchToProps = dispatch => {
return {
fetchPermissions: (namespace: string, repoName: string) => {
dispatch(fetchPermissions(namespace, repoName));
},
createPermission: (
permission: PermissionEntry,
namespace: string,
repoName: string,
callback?: () => void
) => {
dispatch(createPermission(permission, namespace, repoName, callback));
},
createPermissionReset: (namespace: string, repoName: string) => {
dispatch(createPermissionReset(namespace, repoName));
},
modifyPermissionReset: (namespace: string, repoName: string) => {
dispatch(modifyPermissionReset(namespace, repoName));
},
deletePermissionReset: (namespace: string, repoName: string) => {
dispatch(deletePermissionReset(namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(Permissions));

View File

@@ -0,0 +1,176 @@
// @flow
import React from "react";
import type { Permission } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import {
modifyPermission,
isModifyPermissionPending,
deletePermission,
isDeletePermissionPending
} from "../modules/permissions";
import { connect } from "react-redux";
import type { History } from "history";
import { Checkbox } from "@scm-manager/ui-components";
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
import TypeSelector from "../components/TypeSelector";
type Props = {
submitForm: Permission => void,
modifyPermission: (Permission, string, string) => void,
permission: Permission,
t: string => string,
namespace: string,
repoName: string,
match: any,
history: History,
loading: boolean,
deletePermission: (Permission, string, string) => void,
deleteLoading: boolean
};
type State = {
permission: Permission
};
class SinglePermission extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
permission: {
name: "",
type: "READ",
groupPermission: false,
_links: {}
}
};
}
componentDidMount() {
const { permission } = this.props;
if (permission) {
this.setState({
permission: {
name: permission.name,
type: permission.type,
groupPermission: permission.groupPermission,
_links: permission._links
}
});
}
}
deletePermission = () => {
this.props.deletePermission(
this.props.permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const { permission } = this.state;
const { loading, namespace, repoName } = this.props;
const typeSelector =
this.props.permission._links && this.props.permission._links.update ? (
<td>
<TypeSelector
handleTypeChange={this.handleTypeChange}
type={permission.type ? permission.type : "READ"}
loading={loading}
/>
</td>
) : (
<td>{permission.type}</td>
);
return (
<tr>
<td>{permission.name}</td>
<td>
<Checkbox checked={permission ? permission.groupPermission : false} />
</td>
{typeSelector}
<td>
<DeletePermissionButton
permission={permission}
namespace={namespace}
repoName={repoName}
deletePermission={this.deletePermission}
loading={this.props.deleteLoading}
/>
</td>
</tr>
);
}
handleTypeChange = (type: string) => {
this.setState({
permission: {
...this.state.permission,
type: type
}
});
this.modifyPermission(type);
};
modifyPermission = (type: string) => {
let permission = this.state.permission;
permission.type = type;
this.props.modifyPermission(
permission,
this.props.namespace,
this.props.repoName
);
};
createSelectOptions(types: string[]) {
return types.map(type => {
return {
label: type,
value: type
};
});
}
}
const mapStateToProps = (state, ownProps) => {
const permission = ownProps.permission;
const loading = isModifyPermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
const deleteLoading = isDeletePermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
return { loading, deleteLoading };
};
const mapDispatchToProps = dispatch => {
return {
modifyPermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(modifyPermission(permission, namespace, repoName));
},
deletePermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(deletePermission(permission, namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(SinglePermission));

View File

@@ -0,0 +1,624 @@
// @flow
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../../modules/types";
import type { Action } from "@scm-manager/ui-components";
import type {
PermissionCollection,
Permission,
PermissionEntry
} from "@scm-manager/ui-types";
import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure";
import { Dispatch } from "redux";
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
types.PENDING_SUFFIX
}`;
export const FETCH_PERMISSIONS_SUCCESS = `${FETCH_PERMISSIONS}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_PERMISSIONS_FAILURE = `${FETCH_PERMISSIONS}_${
types.FAILURE_SUFFIX
}`;
export const MODIFY_PERMISSION = "scm/permissions/MODFIY_PERMISSION";
export const MODIFY_PERMISSION_PENDING = `${MODIFY_PERMISSION}_${
types.PENDING_SUFFIX
}`;
export const MODIFY_PERMISSION_SUCCESS = `${MODIFY_PERMISSION}_${
types.SUCCESS_SUFFIX
}`;
export const MODIFY_PERMISSION_FAILURE = `${MODIFY_PERMISSION}_${
types.FAILURE_SUFFIX
}`;
export const MODIFY_PERMISSION_RESET = `${MODIFY_PERMISSION}_${
types.RESET_SUFFIX
}`;
export const CREATE_PERMISSION = "scm/permissions/CREATE_PERMISSION";
export const CREATE_PERMISSION_PENDING = `${CREATE_PERMISSION}_${
types.PENDING_SUFFIX
}`;
export const CREATE_PERMISSION_SUCCESS = `${CREATE_PERMISSION}_${
types.SUCCESS_SUFFIX
}`;
export const CREATE_PERMISSION_FAILURE = `${CREATE_PERMISSION}_${
types.FAILURE_SUFFIX
}`;
export const CREATE_PERMISSION_RESET = `${CREATE_PERMISSION}_${
types.RESET_SUFFIX
}`;
export const DELETE_PERMISSION = "scm/permissions/DELETE_PERMISSION";
export const DELETE_PERMISSION_PENDING = `${DELETE_PERMISSION}_${
types.PENDING_SUFFIX
}`;
export const DELETE_PERMISSION_SUCCESS = `${DELETE_PERMISSION}_${
types.SUCCESS_SUFFIX
}`;
export const DELETE_PERMISSION_FAILURE = `${DELETE_PERMISSION}_${
types.FAILURE_SUFFIX
}`;
export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
types.RESET_SUFFIX
}`;
const REPOS_URL = "repositories";
const PERMISSIONS_URL = "permissions";
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
// fetch permissions
export function fetchPermissions(namespace: string, repoName: string) {
return function(dispatch: any) {
dispatch(fetchPermissionsPending(namespace, repoName));
return apiClient
.get(`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`)
.then(response => response.json())
.then(permissions => {
dispatch(fetchPermissionsSuccess(permissions, namespace, repoName));
})
.catch(err => {
dispatch(fetchPermissionsFailure(namespace, repoName, err));
});
};
}
export function fetchPermissionsPending(
namespace: string,
repoName: string
): Action {
return {
type: FETCH_PERMISSIONS_PENDING,
payload: {
namespace,
repoName
},
itemId: namespace + "/" + repoName
};
}
export function fetchPermissionsSuccess(
permissions: any,
namespace: string,
repoName: string
): Action {
return {
type: FETCH_PERMISSIONS_SUCCESS,
payload: permissions,
itemId: namespace + "/" + repoName
};
}
export function fetchPermissionsFailure(
namespace: string,
repoName: string,
error: Error
): Action {
return {
type: FETCH_PERMISSIONS_FAILURE,
payload: {
namespace,
repoName,
error
},
itemId: namespace + "/" + repoName
};
}
// modify permission
export function modifyPermission(
permission: Permission,
namespace: string,
repoName: string,
callback?: () => void
) {
return function(dispatch: any) {
dispatch(modifyPermissionPending(permission, namespace, repoName));
return apiClient
.put(permission._links.update.href, permission, CONTENT_TYPE)
.then(() => {
dispatch(modifyPermissionSuccess(permission, namespace, repoName));
if (callback) {
callback();
}
})
.catch(cause => {
const error = new Error(
`failed to modify permission: ${cause.message}`
);
dispatch(
modifyPermissionFailure(permission, error, namespace, repoName)
);
});
};
}
export function modifyPermissionPending(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: MODIFY_PERMISSION_PENDING,
payload: permission,
itemId: createItemId(permission, namespace, repoName)
};
}
export function modifyPermissionSuccess(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: MODIFY_PERMISSION_SUCCESS,
payload: {
permission,
position: namespace + "/" + repoName
},
itemId: createItemId(permission, namespace, repoName)
};
}
export function modifyPermissionFailure(
permission: Permission,
error: Error,
namespace: string,
repoName: string
): Action {
return {
type: MODIFY_PERMISSION_FAILURE,
payload: { error, permission },
itemId: createItemId(permission, namespace, repoName)
};
}
function newPermissions(
oldPermissions: PermissionCollection,
newPermission: Permission
) {
for (let i = 0; i < oldPermissions.length; i++) {
if (oldPermissions[i].name === newPermission.name) {
oldPermissions.splice(i, 1, newPermission);
return oldPermissions;
}
}
}
export function modifyPermissionReset(namespace: string, repoName: string) {
return {
type: MODIFY_PERMISSION_RESET,
payload: {
namespace,
repoName
},
itemId: namespace + "/" + repoName
};
}
// create permission
export function createPermission(
permission: PermissionEntry,
namespace: string,
repoName: string,
callback?: () => void
) {
return function(dispatch: Dispatch) {
dispatch(createPermissionPending(permission, namespace, repoName));
return apiClient
.post(
`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`,
permission,
CONTENT_TYPE
)
.then(response => {
const location = response.headers.get("Location");
return apiClient.get(location);
})
.then(response => response.json())
.then(createdPermission => {
dispatch(
createPermissionSuccess(createdPermission, namespace, repoName)
);
if (callback) {
callback();
}
})
.catch(err =>
dispatch(
createPermissionFailure(
new Error(
`failed to add permission ${permission.name}: ${err.message}`
),
namespace,
repoName
)
)
);
};
}
export function createPermissionPending(
permission: PermissionEntry,
namespace: string,
repoName: string
): Action {
return {
type: CREATE_PERMISSION_PENDING,
payload: permission,
itemId: namespace + "/" + repoName
};
}
export function createPermissionSuccess(
permission: PermissionEntry,
namespace: string,
repoName: string
): Action {
return {
type: CREATE_PERMISSION_SUCCESS,
payload: {
permission,
position: namespace + "/" + repoName
},
itemId: namespace + "/" + repoName
};
}
export function createPermissionFailure(
error: Error,
namespace: string,
repoName: string
): Action {
return {
type: CREATE_PERMISSION_FAILURE,
payload: error,
itemId: namespace + "/" + repoName
};
}
export function createPermissionReset(namespace: string, repoName: string) {
return {
type: CREATE_PERMISSION_RESET,
itemId: namespace + "/" + repoName
};
}
// delete permission
export function deletePermission(
permission: Permission,
namespace: string,
repoName: string,
callback?: () => void
) {
return function(dispatch: any) {
dispatch(deletePermissionPending(permission, namespace, repoName));
return apiClient
.delete(permission._links.delete.href)
.then(() => {
dispatch(deletePermissionSuccess(permission, namespace, repoName));
if (callback) {
callback();
}
})
.catch(cause => {
const error = new Error(
`could not delete permission ${permission.name}: ${cause.message}`
);
dispatch(
deletePermissionFailure(permission, namespace, repoName, error)
);
});
};
}
export function deletePermissionPending(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: DELETE_PERMISSION_PENDING,
payload: permission,
itemId: createItemId(permission, namespace, repoName)
};
}
export function deletePermissionSuccess(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: DELETE_PERMISSION_SUCCESS,
payload: {
permission,
position: namespace + "/" + repoName
},
itemId: createItemId(permission, namespace, repoName)
};
}
export function deletePermissionFailure(
permission: Permission,
namespace: string,
repoName: string,
error: Error
): Action {
return {
type: DELETE_PERMISSION_FAILURE,
payload: {
error,
permission
},
itemId: createItemId(permission, namespace, repoName)
};
}
export function deletePermissionReset(namespace: string, repoName: string) {
return {
type: DELETE_PERMISSION_RESET,
payload: {
namespace,
repoName
},
itemId: namespace + "/" + repoName
};
}
function deletePermissionFromState(
oldPermissions: PermissionCollection,
permission: Permission
) {
let newPermission = [];
for (let i = 0; i < oldPermissions.length; i++) {
if (oldPermissions[i] !== permission) {
newPermission.push(oldPermissions[i]);
}
}
return newPermission;
}
function createItemId(
permission: Permission,
namespace: string,
repoName: string
) {
let groupPermission = permission.groupPermission ? "@" : "";
return namespace + "/" + repoName + "/" + groupPermission + permission.name;
}
// reducer
export default function reducer(
state: Object = {},
action: Action = { type: "UNKNOWN" }
): Object {
if (!action.payload) {
return state;
}
switch (action.type) {
case FETCH_PERMISSIONS_SUCCESS:
return {
...state,
[action.itemId]: {
entries: action.payload._embedded.permissions,
createPermission: action.payload._links.create ? true : false
}
};
case MODIFY_PERMISSION_SUCCESS:
const positionOfPermission = action.payload.position;
const newPermission = newPermissions(
state[action.payload.position].entries,
action.payload.permission
);
return {
...state,
[positionOfPermission]: {
...state[positionOfPermission],
entries: newPermission
}
};
case CREATE_PERMISSION_SUCCESS:
// return state;
const position = action.payload.position;
const permissions = state[action.payload.position].entries;
permissions.push(action.payload.permission);
return {
...state,
[position]: {
...state[position],
entries: permissions
}
};
case DELETE_PERMISSION_SUCCESS:
const permissionPosition = action.payload.position;
const new_Permissions = deletePermissionFromState(
state[action.payload.position].entries,
action.payload.permission
);
return {
...state,
[permissionPosition]: {
...state[permissionPosition],
entries: new_Permissions
}
};
default:
return state;
}
}
// selectors
export function getPermissionsOfRepo(
state: Object,
namespace: string,
repoName: string
) {
if (state.permissions && state.permissions[namespace + "/" + repoName]) {
const permissions = state.permissions[namespace + "/" + repoName].entries;
return permissions;
}
}
export function isFetchPermissionsPending(
state: Object,
namespace: string,
repoName: string
) {
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
}
export function getFetchPermissionsFailure(
state: Object,
namespace: string,
repoName: string
) {
return getFailure(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
}
export function isModifyPermissionPending(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return isPending(
state,
MODIFY_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function getModifyPermissionFailure(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return getFailure(
state,
MODIFY_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function hasCreatePermission(
state: Object,
namespace: string,
repoName: string
) {
if (state.permissions && state.permissions[namespace + "/" + repoName])
return state.permissions[namespace + "/" + repoName].createPermission;
else return null;
}
export function isCreatePermissionPending(
state: Object,
namespace: string,
repoName: string
) {
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
}
export function getCreatePermissionFailure(
state: Object,
namespace: string,
repoName: string
) {
return getFailure(state, CREATE_PERMISSION, namespace + "/" + repoName);
}
export function isDeletePermissionPending(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return isPending(
state,
DELETE_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function getDeletePermissionFailure(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return getFailure(
state,
DELETE_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function getDeletePermissionsFailure(
state: Object,
namespace: string,
repoName: string
) {
const permissions =
state.permissions && state.permissions[namespace + "/" + repoName]
? state.permissions[namespace + "/" + repoName].entries
: null;
if (permissions == null) return undefined;
for (let i = 0; i < permissions.length; i++) {
if (
getDeletePermissionFailure(state, namespace, repoName, permissions[i])
) {
return getFailure(
state,
DELETE_PERMISSION,
createItemId(permissions[i], namespace, repoName)
);
}
}
return null;
}
export function getModifyPermissionsFailure(
state: Object,
namespace: string,
repoName: string
) {
const permissions =
state.permissions && state.permissions[namespace + "/" + repoName]
? state.permissions[namespace + "/" + repoName].entries
: null;
if (permissions == null) return undefined;
for (let i = 0; i < permissions.length; i++) {
if (
getModifyPermissionFailure(state, namespace, repoName, permissions[i])
) {
return getFailure(
state,
MODIFY_PERMISSION,
createItemId(permissions[i], namespace, repoName)
);
}
}
return null;
}

View File

@@ -0,0 +1,769 @@
// @flow
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import reducer, {
fetchPermissions,
fetchPermissionsSuccess,
getPermissionsOfRepo,
isFetchPermissionsPending,
getFetchPermissionsFailure,
modifyPermission,
modifyPermissionSuccess,
getModifyPermissionFailure,
isModifyPermissionPending,
createPermission,
hasCreatePermission,
deletePermission,
deletePermissionSuccess,
getDeletePermissionFailure,
isDeletePermissionPending,
getModifyPermissionsFailure,
MODIFY_PERMISSION_FAILURE,
MODIFY_PERMISSION_PENDING,
FETCH_PERMISSIONS,
FETCH_PERMISSIONS_PENDING,
FETCH_PERMISSIONS_SUCCESS,
FETCH_PERMISSIONS_FAILURE,
MODIFY_PERMISSION_SUCCESS,
MODIFY_PERMISSION,
CREATE_PERMISSION_PENDING,
CREATE_PERMISSION_SUCCESS,
CREATE_PERMISSION_FAILURE,
DELETE_PERMISSION,
DELETE_PERMISSION_PENDING,
DELETE_PERMISSION_SUCCESS,
DELETE_PERMISSION_FAILURE,
CREATE_PERMISSION,
createPermissionSuccess,
getCreatePermissionFailure,
isCreatePermissionPending,
getDeletePermissionsFailure
} from "./permissions";
import type { Permission, PermissionCollection } from "@scm-manager/ui-types";
const hitchhiker_puzzle42Permission_user_eins: Permission = {
name: "user_eins",
type: "READ",
groupPermission: false,
_links: {
self: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
},
delete: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
},
update: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
}
}
};
const hitchhiker_puzzle42Permission_user_zwei: Permission = {
name: "user_zwei",
type: "WRITE",
groupPermission: true,
_links: {
self: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
},
delete: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
},
update: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
}
}
};
const hitchhiker_puzzle42Permissions: PermissionCollection = [
hitchhiker_puzzle42Permission_user_eins,
hitchhiker_puzzle42Permission_user_zwei
];
const hitchhiker_puzzle42RepoPermissions = {
_embedded: {
permissions: hitchhiker_puzzle42Permissions
},
_links: {
create: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions"
}
}
};
describe("permission fetch", () => {
const REPOS_URL = "/api/v2/repositories";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should successfully fetch permissions to repo hitchhiker/puzzle42", () => {
fetchMock.getOnce(
REPOS_URL + "/hitchhiker/puzzle42/permissions",
hitchhiker_puzzle42RepoPermissions
);
const expectedActions = [
{
type: FETCH_PERMISSIONS_PENDING,
payload: {
namespace: "hitchhiker",
repoName: "puzzle42"
},
itemId: "hitchhiker/puzzle42"
},
{
type: FETCH_PERMISSIONS_SUCCESS,
payload: hitchhiker_puzzle42RepoPermissions,
itemId: "hitchhiker/puzzle42"
}
];
const store = mockStore({});
return store
.dispatch(fetchPermissions("hitchhiker", "puzzle42"))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_PERMISSIONS_FAILURE, it the request fails", () => {
fetchMock.getOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 500
});
const store = mockStore({});
return store
.dispatch(fetchPermissions("hitchhiker", "puzzle42"))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING);
expect(actions[1].type).toEqual(FETCH_PERMISSIONS_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should successfully modify user_eins permission", () => {
fetchMock.putOnce(
hitchhiker_puzzle42Permission_user_eins._links.update.href,
{
status: 204
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
editedPermission.type = "OWNER";
const store = mockStore({});
return store
.dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42"))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS);
});
});
it("should successfully modify user_eins permission and call the callback", () => {
fetchMock.putOnce(
hitchhiker_puzzle42Permission_user_eins._links.update.href,
{
status: 204
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
editedPermission.type = "OWNER";
const store = mockStore({});
let called = false;
const callback = () => {
called = true;
};
return store
.dispatch(
modifyPermission(editedPermission, "hitchhiker", "puzzle42", callback)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS);
expect(called).toBe(true);
});
});
it("should fail modifying on HTTP 500", () => {
fetchMock.putOnce(
hitchhiker_puzzle42Permission_user_eins._links.update.href,
{
status: 500
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
editedPermission.type = "OWNER";
const store = mockStore({});
return store
.dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42"))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
expect(actions[1].type).toEqual(MODIFY_PERMISSION_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should add a permission successfully", () => {
// unmatched
fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 204,
headers: {
location: "repositories/hitchhiker/puzzle42/permissions/user_eins"
}
});
fetchMock.getOnce(
REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins",
hitchhiker_puzzle42Permission_user_eins
);
const store = mockStore({});
return store
.dispatch(
createPermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING);
expect(actions[1].type).toEqual(CREATE_PERMISSION_SUCCESS);
});
});
it("should fail adding a permission on HTTP 500", () => {
fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 500
});
const store = mockStore({});
return store
.dispatch(
createPermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING);
expect(actions[1].type).toEqual(CREATE_PERMISSION_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should call the callback after permission successfully created", () => {
// unmatched
fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 204,
headers: {
location: "repositories/hitchhiker/puzzle42/permissions/user_eins"
}
});
fetchMock.getOnce(
REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins",
hitchhiker_puzzle42Permission_user_eins
);
let callMe = "not yet";
const callback = () => {
callMe = "yeah";
};
const store = mockStore({});
return store
.dispatch(
createPermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42",
callback
)
)
.then(() => {
expect(callMe).toBe("yeah");
});
});
it("should delete successfully permission user_eins", () => {
fetchMock.deleteOnce(
hitchhiker_puzzle42Permission_user_eins._links.delete.href,
{
status: 204
}
);
const store = mockStore({});
return store
.dispatch(
deletePermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions.length).toBe(2);
expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING);
expect(actions[0].payload).toBe(
hitchhiker_puzzle42Permission_user_eins
);
expect(actions[1].type).toEqual(DELETE_PERMISSION_SUCCESS);
});
});
it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce(
hitchhiker_puzzle42Permission_user_eins._links.delete.href,
{
status: 204
}
);
let called = false;
const callMe = () => {
called = true;
};
const store = mockStore({});
return store
.dispatch(
deletePermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42",
callMe
)
)
.then(() => {
expect(called).toBeTruthy();
});
});
it("should fail to delete permission", () => {
fetchMock.deleteOnce(
hitchhiker_puzzle42Permission_user_eins._links.delete.href,
{
status: 500
}
);
const store = mockStore({});
return store
.dispatch(
deletePermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING);
expect(actions[0].payload).toBe(
hitchhiker_puzzle42Permission_user_eins
);
expect(actions[1].type).toEqual(DELETE_PERMISSION_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
});
describe("permissions reducer", () => {
it("should return empty object, if state and action is undefined", () => {
expect(reducer()).toEqual({});
});
it("should return the same state, if the action is undefined", () => {
const state = { x: true };
expect(reducer(state)).toBe(state);
});
it("should return the same state, if the action is unknown to the reducer", () => {
const state = { x: true };
expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state);
});
it("should store the permissions on FETCH_PERMISSION_SUCCESS", () => {
const newState = reducer(
{},
fetchPermissionsSuccess(
hitchhiker_puzzle42RepoPermissions,
"hitchhiker",
"puzzle42"
)
);
expect(newState["hitchhiker/puzzle42"].entries).toBe(
hitchhiker_puzzle42Permissions
);
});
it("should update permission", () => {
const oldState = {
"hitchhiker/puzzle42": {
entries: [hitchhiker_puzzle42Permission_user_eins]
}
};
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins };
permissionEdited.type = "OWNER";
let expectedState = {
"hitchhiker/puzzle42": {
entries: [permissionEdited]
}
};
const newState = reducer(
oldState,
modifyPermissionSuccess(permissionEdited, "hitchhiker", "puzzle42")
);
expect(newState["hitchhiker/puzzle42"]).toEqual(
expectedState["hitchhiker/puzzle42"]
);
});
it("should remove permission from state when delete succeeds", () => {
const state = {
"hitchhiker/puzzle42": {
entries: [
hitchhiker_puzzle42Permission_user_eins,
hitchhiker_puzzle42Permission_user_zwei
]
}
};
const expectedState = {
"hitchhiker/puzzle42": {
entries: [hitchhiker_puzzle42Permission_user_zwei]
}
};
const newState = reducer(
state,
deletePermissionSuccess(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
);
expect(newState["hitchhiker/puzzle42"]).toEqual(
expectedState["hitchhiker/puzzle42"]
);
});
it("should add permission", () => {
//changing state had to be removed because of errors
const oldState = {
"hitchhiker/puzzle42": {
entries: [hitchhiker_puzzle42Permission_user_eins]
}
};
let expectedState = {
"hitchhiker/puzzle42": {
entries: [
hitchhiker_puzzle42Permission_user_eins,
hitchhiker_puzzle42Permission_user_zwei
]
}
};
const newState = reducer(
oldState,
createPermissionSuccess(
hitchhiker_puzzle42Permission_user_zwei,
"hitchhiker",
"puzzle42"
)
);
expect(newState["hitchhiker/puzzle42"]).toEqual(
expectedState["hitchhiker/puzzle42"]
);
});
});
describe("permissions selectors", () => {
const error = new Error("something goes wrong");
it("should return the permissions of one repository", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": {
entries: hitchhiker_puzzle42Permissions
}
}
};
const repoPermissions = getPermissionsOfRepo(
state,
"hitchhiker",
"puzzle42"
);
expect(repoPermissions).toEqual(hitchhiker_puzzle42Permissions);
});
it("should return true, when fetch permissions is pending", () => {
const state = {
pending: {
[FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: true
}
};
expect(isFetchPermissionsPending(state, "hitchhiker", "puzzle42")).toEqual(
true
);
});
it("should return false, when fetch permissions is not pending", () => {
expect(isFetchPermissionsPending({}, "hitchiker", "puzzle42")).toEqual(
false
);
});
it("should return error when fetch permissions did fail", () => {
const state = {
failure: {
[FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: error
}
};
expect(getFetchPermissionsFailure(state, "hitchhiker", "puzzle42")).toEqual(
error
);
});
it("should return undefined when fetch permissions did not fail", () => {
expect(getFetchPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
it("should return true, when modify permission is pending", () => {
const state = {
pending: {
[MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true
}
};
expect(
isModifyPermissionPending(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(true);
});
it("should return false, when modify permission is not pending", () => {
expect(
isModifyPermissionPending(
{},
"hitchiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(false);
});
it("should return error when modify permission did fail", () => {
const state = {
failure: {
[MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getModifyPermissionFailure(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(error);
});
it("should return undefined when modify permission did not fail", () => {
expect(
getModifyPermissionFailure(
{},
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toBe(undefined);
});
it("should return error when one of the modify permissions did fail", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions }
},
failure: {
[MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getModifyPermissionsFailure(state, "hitchhiker", "puzzle42")
).toEqual(error);
});
it("should return undefined when no modify permissions did not fail", () => {
expect(getModifyPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
it("should return true, when createPermission is true", () => {
const state = {
permissions: {
["hitchhiker/puzzle42"]: {
createPermission: true
}
}
};
expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toBe(true);
});
it("should return false, when createPermission is false", () => {
const state = {
permissions: {
["hitchhiker/puzzle42"]: {
createPermission: false
}
}
};
expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toEqual(false);
});
it("should return true, when delete permission is pending", () => {
const state = {
pending: {
[DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true
}
};
expect(
isDeletePermissionPending(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(true);
});
it("should return false, when delete permission is not pending", () => {
expect(
isDeletePermissionPending(
{},
"hitchiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(false);
});
it("should return error when delete permission did fail", () => {
const state = {
failure: {
[DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getDeletePermissionFailure(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(error);
});
it("should return undefined when delete permission did not fail", () => {
expect(
getDeletePermissionFailure(
{},
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toBe(undefined);
});
it("should return error when one of the delete permissions did fail", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions }
},
failure: {
[DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getDeletePermissionsFailure(state, "hitchhiker", "puzzle42")
).toEqual(error);
});
it("should return undefined when no delete permissions did not fail", () => {
expect(getDeletePermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
it("should return true, when create permission is pending", () => {
const state = {
pending: {
[CREATE_PERMISSION + "/hitchhiker/puzzle42"]: true
}
};
expect(isCreatePermissionPending(state, "hitchhiker", "puzzle42")).toEqual(
true
);
});
it("should return false, when create permissions is not pending", () => {
expect(isCreatePermissionPending({}, "hitchiker", "puzzle42")).toEqual(
false
);
});
it("should return error when create permissions did fail", () => {
const state = {
failure: {
[CREATE_PERMISSION + "/hitchhiker/puzzle42"]: error
}
};
expect(getCreatePermissionFailure(state, "hitchhiker", "puzzle42")).toEqual(
error
);
});
it("should return undefined when create permissions did not fail", () => {
expect(getCreatePermissionFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
});

View File

@@ -97,6 +97,7 @@ class UserForm extends React.Component<Props, State> {
value={user ? user.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
helpText={t("help.usernameHelpText")}
/>
);
}
@@ -109,6 +110,7 @@ class UserForm extends React.Component<Props, State> {
value={user ? user.displayName : ""}
validationError={this.state.displayNameValidationError}
errorMessage={t("validation.displayname-invalid")}
helpText={t("help.displayNameHelpText")}
/>
<InputField
label={t("user.mail")}
@@ -116,6 +118,7 @@ class UserForm extends React.Component<Props, State> {
value={user ? user.mail : ""}
validationError={this.state.mailValidationError}
errorMessage={t("validation.mail-invalid")}
helpText={t("help.mailHelpText")}
/>
<InputField
label={t("user.password")}
@@ -124,6 +127,7 @@ class UserForm extends React.Component<Props, State> {
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
@@ -132,16 +136,19 @@ class UserForm extends React.Component<Props, State> {
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordValidationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
<Checkbox
label={t("user.admin")}
onChange={this.handleAdminChange}
checked={user ? user.admin : false}
helpText={t("help.adminHelpText")}
/>
<Checkbox
label={t("user.active")}
onChange={this.handleActiveChange}
checked={user ? user.active : false}
helpText={t("help.activeHelpText")}
/>
<SubmitButton
disabled={!this.isValid()}

View File

@@ -13,5 +13,5 @@ export const isDisplayNameValid = (displayName: string) => {
return false;
};
export const isPasswordValid = (password: string) => {
return password.length > 6 && password.length < 32;
return password.length >= 6 && password.length < 32;
};

View File

@@ -61,13 +61,13 @@ const userZaphod = {
properties: {},
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/users/zaphod"
href: "http://localhost:8081/api/v2/users/zaphod"
},
delete: {
href: "http://localhost:8081/api/rest/v2/users/zaphod"
href: "http://localhost:8081/api/v2/users/zaphod"
},
update: {
href: "http://localhost:8081/api/rest/v2/users/zaphod"
href: "http://localhost:8081/api/v2/users/zaphod"
}
}
};
@@ -84,13 +84,13 @@ const userFord = {
properties: {},
_links: {
self: {
href: "http://localhost:8081/api/rest/v2/users/ford"
href: "http://localhost:8081/api/v2/users/ford"
},
delete: {
href: "http://localhost:8081/api/rest/v2/users/ford"
href: "http://localhost:8081/api/v2/users/ford"
},
update: {
href: "http://localhost:8081/api/rest/v2/users/ford"
href: "http://localhost:8081/api/v2/users/ford"
}
}
};
@@ -100,16 +100,16 @@ const responseBody = {
pageTotal: 1,
_links: {
self: {
href: "http://localhost:3000/api/rest/v2/users/?page=0&pageSize=10"
href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
first: {
href: "http://localhost:3000/api/rest/v2/users/?page=0&pageSize=10"
href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
last: {
href: "http://localhost:3000/api/rest/v2/users/?page=0&pageSize=10"
href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
create: {
href: "http://localhost:3000/api/rest/v2/users/"
href: "http://localhost:3000/api/v2/users/"
}
},
_embedded: {
@@ -122,7 +122,7 @@ const response = {
responseBody
};
const USERS_URL = "/api/rest/v2/users";
const USERS_URL = "/api/v2/users";
const error = new Error("KAPUTT");
@@ -241,7 +241,7 @@ describe("users fetch()", () => {
});
it("successfully update user", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -255,7 +255,7 @@ describe("users fetch()", () => {
});
it("should call callback, after successful modified user", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -271,7 +271,7 @@ describe("users fetch()", () => {
});
it("should fail updating user on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 500
});
@@ -285,7 +285,7 @@ describe("users fetch()", () => {
});
it("should delete successfully user zaphod", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -300,7 +300,7 @@ describe("users fetch()", () => {
});
it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -316,7 +316,7 @@ describe("users fetch()", () => {
});
it("should fail to delete user zaphod", () => {
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 500
});

View File

@@ -35,6 +35,7 @@ $blue: #33B2E8;
// 6. Import the rest of Bulma
@import "bulma/bulma";
@import "bulma-tooltip/dist/css/bulma-tooltip";
// import at the end, because we need a lot of stuff from bulma/bulma
.box-link-shadow {

File diff suppressed because it is too large Load Diff

View File

@@ -62,7 +62,7 @@
<modules>
<jaxrs datatype-detection="local">
<application path="/api/rest" />
<application path="/api" />
</jaxrs>
<docs disableResourceLinks="true" includeApplicationPath="true" />

View File

@@ -152,7 +152,7 @@ public class ScmServletModule extends ServletModule
public static final String PATTERN_PLUGIN_SCRIPT = "/plugins/resources/js/*";
/** Field description */
public static final String PATTERN_RESTAPI = "/api/rest/*";
public static final String PATTERN_RESTAPI = "/api/*";
/** Field description */
public static final String PATTERN_SCRIPT = "*.js";

Some files were not shown because too many files have changed in this diff Show More