mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 16:05:44 +01:00
Merge with default
This commit is contained in:
4
Jenkinsfile
vendored
4
Jenkinsfile
vendored
@@ -7,7 +7,7 @@ import com.cloudogu.ces.cesbuildlib.*
|
|||||||
node('docker') {
|
node('docker') {
|
||||||
|
|
||||||
// Change this as when we go back to default - necessary for proper SonarQube analysis
|
// Change this as when we go back to default - necessary for proper SonarQube analysis
|
||||||
mainBranch = '2.0.0-m3'
|
mainBranch = 'default'
|
||||||
|
|
||||||
properties([
|
properties([
|
||||||
// Keep only the last 10 build to preserve space
|
// Keep only the last 10 build to preserve space
|
||||||
@@ -37,7 +37,7 @@ node('docker') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stage('Integration Test') {
|
stage('Integration Test') {
|
||||||
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true'
|
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -DClassLoaderLeakPreventor.threadWaitMs=10'
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('SonarQube') {
|
stage('SonarQube') {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"test": "lerna run --scope '@scm-manager/ui-*' test",
|
"test": "lerna run --scope '@scm-manager/ui-*' test",
|
||||||
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
|
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
|
||||||
"serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
|
"serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
|
||||||
"deploy": "ui-scripts publish"
|
"deploy": "ui-scripts publish",
|
||||||
|
"set-version": "ui-scripts version"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-plugin-reflow": "^0.2.7",
|
"babel-plugin-reflow": "^0.2.7",
|
||||||
|
|||||||
14
pom.xml
14
pom.xml
@@ -220,7 +220,13 @@
|
|||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.resteasy</groupId>
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
<artifactId>resteasy-jaxrs</artifactId>
|
<artifactId>resteasy-core</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-core-spi</artifactId>
|
||||||
<version>${resteasy.version}</version>
|
<version>${resteasy.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -444,7 +450,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>sonia.scm.maven</groupId>
|
<groupId>sonia.scm.maven</groupId>
|
||||||
<artifactId>smp-maven-plugin</artifactId>
|
<artifactId>smp-maven-plugin</artifactId>
|
||||||
<version>1.0.0-alpha-8</version>
|
<version>1.0.0-rc1</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
@@ -831,7 +837,7 @@
|
|||||||
<servlet.version>3.0.1</servlet.version>
|
<servlet.version>3.0.1</servlet.version>
|
||||||
|
|
||||||
<jaxrs.version>2.1.1</jaxrs.version>
|
<jaxrs.version>2.1.1</jaxrs.version>
|
||||||
<resteasy.version>3.6.2.Final</resteasy.version>
|
<resteasy.version>4.4.1.Final</resteasy.version>
|
||||||
<jersey-client.version>1.19.4</jersey-client.version>
|
<jersey-client.version>1.19.4</jersey-client.version>
|
||||||
<enunciate.version>2.11.1</enunciate.version>
|
<enunciate.version>2.11.1</enunciate.version>
|
||||||
<jackson.version>2.10.0</jackson.version>
|
<jackson.version>2.10.0</jackson.version>
|
||||||
@@ -839,7 +845,7 @@
|
|||||||
<jaxb.version>2.3.0</jaxb.version>
|
<jaxb.version>2.3.0</jaxb.version>
|
||||||
|
|
||||||
<!-- event bus -->
|
<!-- event bus -->
|
||||||
<legman.version>1.5.1</legman.version>
|
<legman.version>1.6.1</legman.version>
|
||||||
|
|
||||||
<!-- webserver -->
|
<!-- webserver -->
|
||||||
<jetty.version>9.4.22.v20191022</jetty.version>
|
<jetty.version>9.4.22.v20191022</jetty.version>
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ import javax.xml.transform.TransformerFactory;
|
|||||||
import javax.xml.transform.dom.DOMSource;
|
import javax.xml.transform.dom.DOMSource;
|
||||||
import javax.xml.transform.stream.StreamResult;
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
|
||||||
|
import static javax.lang.model.util.ElementFilter.methodsIn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@SupportedAnnotationTypes("*")
|
@SupportedAnnotationTypes("*")
|
||||||
@@ -372,6 +373,18 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
|
|||||||
attributes.put(entry.getKey().getSimpleName().toString(),
|
attributes.put(entry.getKey().getSimpleName().toString(),
|
||||||
getValue(entry.getValue()));
|
getValue(entry.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add default values
|
||||||
|
for (ExecutableElement meth : methodsIn(annotationMirror.getAnnotationType().asElement().getEnclosedElements())) {
|
||||||
|
String attribute = meth.getSimpleName().toString();
|
||||||
|
AnnotationValue defaultValue = meth.getDefaultValue();
|
||||||
|
if (defaultValue != null && !attributes.containsKey(attribute)) {
|
||||||
|
String value = getValue(defaultValue);
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
attributes.put(attribute, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
<artifactId>scm</artifactId>
|
<artifactId>scm</artifactId>
|
||||||
<version>2.0.0-SNAPSHOT</version>
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>sonia.scm</groupId>
|
|
||||||
<artifactId>scm-annotations</artifactId>
|
<artifactId>scm-annotations</artifactId>
|
||||||
<version>2.0.0-SNAPSHOT</version>
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
<name>scm-annotations</name>
|
<name>scm-annotations</name>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.resteasy</groupId>
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
<artifactId>resteasy-jaxrs</artifactId>
|
<artifactId>resteasy-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public final class SCMContext
|
|||||||
*/
|
*/
|
||||||
public static final User ANONYMOUS = new User(USER_ANONYMOUS,
|
public static final User ANONYMOUS = new User(USER_ANONYMOUS,
|
||||||
"SCM Anonymous",
|
"SCM Anonymous",
|
||||||
"scm-anonymous@scm-manager.com");
|
"scm-anonymous@scm-manager.org");
|
||||||
|
|
||||||
/** Singleton instance of {@link SCMContextProvider} */
|
/** Singleton instance of {@link SCMContextProvider} */
|
||||||
private static volatile SCMContextProvider provider;
|
private static volatile SCMContextProvider provider;
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ public final class IterableQueue<T> implements Iterable<T>
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
logger.trace("create queue iterator");
|
logger.trace("create queue iterator");
|
||||||
iterator = new QueueIterator<T>(this);
|
iterator = new QueueIterator<>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
return iterator;
|
return iterator;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class LimitedSortedSet<E> extends ForwardingSortedSet<E>
|
|||||||
*/
|
*/
|
||||||
public LimitedSortedSet(int maxSize)
|
public LimitedSortedSet(int maxSize)
|
||||||
{
|
{
|
||||||
this.sortedSet = new TreeSet<E>();
|
this.sortedSet = new TreeSet<>();
|
||||||
this.maxSize = maxSize;
|
this.maxSize = maxSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,5 +183,5 @@ public abstract class AbstractResourceProcessor implements ResourceProcessor
|
|||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private Map<String, String> variableMap = new HashMap<String, String>();
|
private Map<String, String> variableMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class INIConfiguration
|
|||||||
*/
|
*/
|
||||||
public INIConfiguration()
|
public INIConfiguration()
|
||||||
{
|
{
|
||||||
this.sectionMap = new LinkedHashMap<String, INISection>();
|
this.sectionMap = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class INISection
|
|||||||
public INISection(String name)
|
public INISection(String name)
|
||||||
{
|
{
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.parameters = new LinkedHashMap<String, String>();
|
this.parameters = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class AvailablePlugin implements Plugin {
|
|||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AvailablePlugin install() {
|
AvailablePlugin install() {
|
||||||
Preconditions.checkState(!pending, "installation is already pending");
|
Preconditions.checkState(!pending, "installation is already pending");
|
||||||
return new AvailablePlugin(pluginDescriptor, true);
|
return new AvailablePlugin(pluginDescriptor, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler
|
|||||||
Repository repository = new Repository();
|
Repository repository = new Repository();
|
||||||
|
|
||||||
repository.setName(repositoryName);
|
repository.setName(repositoryName);
|
||||||
repository.setPublicReadable(false);
|
|
||||||
repository.setType(getTypeName());
|
repository.setType(getTypeName());
|
||||||
|
|
||||||
return repository;
|
return repository;
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
private String name;
|
private String name;
|
||||||
@XmlElement(name = "permission")
|
@XmlElement(name = "permission")
|
||||||
private Set<RepositoryPermission> permissions = new HashSet<>();
|
private Set<RepositoryPermission> permissions = new HashSet<>();
|
||||||
@XmlElement(name = "public")
|
|
||||||
private boolean publicReadable = false;
|
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
|
|
||||||
@@ -225,15 +223,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
return Util.isEmpty(healthCheckFailures);
|
return Util.isEmpty(healthCheckFailures);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the {@link Repository} is public readable.
|
|
||||||
*
|
|
||||||
* @return true if the {@link Repository} is public readable
|
|
||||||
*/
|
|
||||||
public boolean isPublicReadable() {
|
|
||||||
return publicReadable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the {@link Repository} is valid.
|
* Returns true if the {@link Repository} is valid.
|
||||||
* <ul>
|
* <ul>
|
||||||
@@ -292,10 +281,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
return this.permissions.remove(permission);
|
return this.permissions.remove(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPublicReadable(boolean publicReadable) {
|
|
||||||
this.publicReadable = publicReadable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setType(String type) {
|
public void setType(String type) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
@@ -332,7 +317,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
repository.setLastModified(lastModified);
|
repository.setLastModified(lastModified);
|
||||||
repository.setDescription(description);
|
repository.setDescription(description);
|
||||||
repository.setPermissions(permissions);
|
repository.setPermissions(permissions);
|
||||||
repository.setPublicReadable(publicReadable);
|
|
||||||
|
|
||||||
// do not copy health check results
|
// do not copy health check results
|
||||||
}
|
}
|
||||||
@@ -360,7 +344,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
&& Objects.equal(name, other.name)
|
&& Objects.equal(name, other.name)
|
||||||
&& Objects.equal(contact, other.contact)
|
&& Objects.equal(contact, other.contact)
|
||||||
&& Objects.equal(description, other.description)
|
&& Objects.equal(description, other.description)
|
||||||
&& Objects.equal(publicReadable, other.publicReadable)
|
|
||||||
&& Objects.equal(permissions, other.permissions)
|
&& Objects.equal(permissions, other.permissions)
|
||||||
&& Objects.equal(type, other.type)
|
&& Objects.equal(type, other.type)
|
||||||
&& Objects.equal(creationDate, other.creationDate)
|
&& Objects.equal(creationDate, other.creationDate)
|
||||||
@@ -371,7 +354,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode(id, namespace, name, contact, description, publicReadable,
|
return Objects.hashCode(id, namespace, name, contact, description,
|
||||||
permissions, type, creationDate, lastModified, properties,
|
permissions, type, creationDate, lastModified, properties,
|
||||||
healthCheckFailures);
|
healthCheckFailures);
|
||||||
}
|
}
|
||||||
@@ -384,7 +367,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
.add("name", name)
|
.add("name", name)
|
||||||
.add("contact", contact)
|
.add("contact", contact)
|
||||||
.add("description", description)
|
.add("description", description)
|
||||||
.add("publicReadable", publicReadable)
|
|
||||||
.add("permissions", permissions)
|
.add("permissions", permissions)
|
||||||
.add("type", type)
|
.add("type", type)
|
||||||
.add("lastModified", lastModified)
|
.add("lastModified", lastModified)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public final class Base32 extends AbstractBase
|
|||||||
{
|
{
|
||||||
|
|
||||||
/** base value */
|
/** base value */
|
||||||
private static final BigInteger BASE = BigInteger.valueOf(32l);
|
private static final BigInteger BASE = BigInteger.valueOf(32L);
|
||||||
|
|
||||||
/** char table */
|
/** char table */
|
||||||
private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz";
|
private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz";
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public final class Base62 extends AbstractBase
|
|||||||
{
|
{
|
||||||
|
|
||||||
/** base value */
|
/** base value */
|
||||||
private static final BigInteger BASE = BigInteger.valueOf(62l);
|
private static final BigInteger BASE = BigInteger.valueOf(62L);
|
||||||
|
|
||||||
/** char table */
|
/** char table */
|
||||||
private static final String CHARS =
|
private static final String CHARS =
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public final class LinkTextParser
|
|||||||
public static String parseText(String content)
|
public static String parseText(String content)
|
||||||
{
|
{
|
||||||
Matcher m = REGEX_URL.matcher(content);
|
Matcher m = REGEX_URL.matcher(content);
|
||||||
List<Token> tokens = new ArrayList<Token>();
|
List<Token> tokens = new ArrayList<>();
|
||||||
int position = 0;
|
int position = 0;
|
||||||
String tokenContent = null;
|
String tokenContent = null;
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public final class ServiceUtil
|
|||||||
*/
|
*/
|
||||||
public static <T> List<T> getServices(Class<T> type)
|
public static <T> List<T> getServices(Class<T> type)
|
||||||
{
|
{
|
||||||
List<T> result = new ArrayList<T>();
|
List<T> result = new ArrayList<>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,23 +38,12 @@ package sonia.scm.util;
|
|||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -273,12 +262,12 @@ public final class Util
|
|||||||
Comparator<T> comparator, CollectionAppender<T> appender, int start,
|
Comparator<T> comparator, CollectionAppender<T> appender, int start,
|
||||||
int limit)
|
int limit)
|
||||||
{
|
{
|
||||||
List<T> result = new ArrayList<T>();
|
List<T> result = new ArrayList<>();
|
||||||
List<T> valueList = new ArrayList<T>(values);
|
List<T> valueList = new ArrayList<>(values);
|
||||||
|
|
||||||
if (comparator != null)
|
if (comparator != null)
|
||||||
{
|
{
|
||||||
Collections.sort(valueList, comparator);
|
valueList.sort(comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
int length = valueList.size();
|
int length = valueList.size();
|
||||||
@@ -506,12 +495,10 @@ public final class Util
|
|||||||
{
|
{
|
||||||
StringBuilder buffer = new StringBuilder();
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
|
||||||
for (int i = 0; i < byteValue.length; i++)
|
for (final byte aByteValue : byteValue) {
|
||||||
{
|
int x = aByteValue & 0xff;
|
||||||
int x = byteValue[i] & 0xff;
|
|
||||||
|
|
||||||
if (x < 16)
|
if (x < 16) {
|
||||||
{
|
|
||||||
buffer.append('0');
|
buffer.append('0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class EnvList
|
|||||||
*/
|
*/
|
||||||
public EnvList()
|
public EnvList()
|
||||||
{
|
{
|
||||||
envMap = new HashMap<String, String>();
|
envMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +77,7 @@ public class EnvList
|
|||||||
*/
|
*/
|
||||||
public EnvList(EnvList l)
|
public EnvList(EnvList l)
|
||||||
{
|
{
|
||||||
envMap = new HashMap<String, String>(l.envMap);
|
envMap = new HashMap<>(l.envMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|||||||
@@ -470,13 +470,13 @@ public class BufferedHttpServletResponse extends HttpServletResponseWrapper
|
|||||||
private ByteArrayPrintWriter pw = null;
|
private ByteArrayPrintWriter pw = null;
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private Set<Cookie> cookies = new HashSet<Cookie>();
|
private Set<Cookie> cookies = new HashSet<>();
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private int statusCode = HttpServletResponse.SC_OK;
|
private int statusCode = HttpServletResponse.SC_OK;
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private Map<String, String> headers = new LinkedHashMap<String, String>();
|
private Map<String, String> headers = new LinkedHashMap<>();
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private String statusMessage;
|
private String statusMessage;
|
||||||
|
|||||||
@@ -40,28 +40,20 @@ import com.google.common.io.ByteStreams;
|
|||||||
import com.google.common.io.Closer;
|
import com.google.common.io.Closer;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.BufferedInputStream;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServlet;
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -175,10 +167,8 @@ public class ProxyServlet extends HttpServlet
|
|||||||
private void copyContent(HttpURLConnection con, HttpServletResponse response)
|
private void copyContent(HttpURLConnection con, HttpServletResponse response)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
Closer closer = Closer.create();
|
|
||||||
|
|
||||||
try
|
try (Closer closer = Closer.create()) {
|
||||||
{
|
|
||||||
InputStream webToProxyBuf =
|
InputStream webToProxyBuf =
|
||||||
closer.register(new BufferedInputStream(con.getInputStream()));
|
closer.register(new BufferedInputStream(con.getInputStream()));
|
||||||
OutputStream proxyToClientBuf =
|
OutputStream proxyToClientBuf =
|
||||||
@@ -188,10 +178,6 @@ public class ProxyServlet extends HttpServlet
|
|||||||
|
|
||||||
logger.trace("copied {} bytes for proxy", bytes);
|
logger.trace("copied {} bytes for proxy", bytes);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
closer.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public class XmlMapStringAdapter
|
|||||||
public Map<String, String> unmarshal(XmlMapStringElement[] elements)
|
public Map<String, String> unmarshal(XmlMapStringElement[] elements)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
Map<String, String> map = new HashMap<String, String>();
|
Map<String, String> map = new HashMap<>();
|
||||||
|
|
||||||
if (elements != null)
|
if (elements != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class XmlSetStringAdapter extends XmlAdapter<String, Set<String>>
|
|||||||
@Override
|
@Override
|
||||||
public Set<String> unmarshal(String rawString) throws Exception
|
public Set<String> unmarshal(String rawString) throws Exception
|
||||||
{
|
{
|
||||||
Set<String> tokens = new HashSet<String>();
|
Set<String> tokens = new HashSet<>();
|
||||||
|
|
||||||
for (String token : rawString.split(","))
|
for (String token : rawString.split(","))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class IterableQueueTest
|
|||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testDuplicatedEndReached()
|
public void testDuplicatedEndReached()
|
||||||
{
|
{
|
||||||
IterableQueue<String> queue = new IterableQueue<String>();
|
IterableQueue<String> queue = new IterableQueue<>();
|
||||||
|
|
||||||
queue.endReached();
|
queue.endReached();
|
||||||
queue.endReached();
|
queue.endReached();
|
||||||
@@ -76,7 +76,7 @@ public class IterableQueueTest
|
|||||||
@Test
|
@Test
|
||||||
public void testIterator()
|
public void testIterator()
|
||||||
{
|
{
|
||||||
IterableQueue<String> queue = new IterableQueue<String>();
|
IterableQueue<String> queue = new IterableQueue<>();
|
||||||
|
|
||||||
assertEquals(QueueIterator.class, queue.iterator().getClass());
|
assertEquals(QueueIterator.class, queue.iterator().getClass());
|
||||||
queue.endReached();
|
queue.endReached();
|
||||||
@@ -120,7 +120,7 @@ public class IterableQueueTest
|
|||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testPushEndReached()
|
public void testPushEndReached()
|
||||||
{
|
{
|
||||||
IterableQueue<String> queue = new IterableQueue<String>();
|
IterableQueue<String> queue = new IterableQueue<>();
|
||||||
|
|
||||||
queue.push("a");
|
queue.push("a");
|
||||||
queue.endReached();
|
queue.endReached();
|
||||||
@@ -134,7 +134,7 @@ public class IterableQueueTest
|
|||||||
@Test
|
@Test
|
||||||
public void testSingleConsumer()
|
public void testSingleConsumer()
|
||||||
{
|
{
|
||||||
final IterableQueue<Integer> queue = new IterableQueue<Integer>();
|
final IterableQueue<Integer> queue = new IterableQueue<>();
|
||||||
|
|
||||||
new Thread(new IntegerProducer(queue, false, 100)).start();
|
new Thread(new IntegerProducer(queue, false, 100)).start();
|
||||||
assertResult(Lists.newArrayList(queue), 100);
|
assertResult(Lists.newArrayList(queue), 100);
|
||||||
@@ -176,12 +176,12 @@ public class IterableQueueTest
|
|||||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||||
List<Future<List<Integer>>> futures = Lists.newArrayList();
|
List<Future<List<Integer>>> futures = Lists.newArrayList();
|
||||||
|
|
||||||
final IterableQueue<Integer> queue = new IterableQueue<Integer>();
|
final IterableQueue<Integer> queue = new IterableQueue<>();
|
||||||
|
|
||||||
for (int i = 0; i < consumer; i++)
|
for (int i = 0; i < consumer; i++)
|
||||||
{
|
{
|
||||||
Future<List<Integer>> future =
|
Future<List<Integer>> future =
|
||||||
executor.submit(new CallableQueueCollector<Integer>(queue));
|
executor.submit(new CallableQueueCollector<>(queue));
|
||||||
|
|
||||||
futures.add(future);
|
futures.add(future);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ public class ScmModuleTest
|
|||||||
assertThat(
|
assertThat(
|
||||||
module.getExtensions(),
|
module.getExtensions(),
|
||||||
containsInAnyOrder(
|
containsInAnyOrder(
|
||||||
(Class<?>) String.class,
|
String.class,
|
||||||
(Class<?>) Integer.class
|
Integer.class
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertThat(
|
assertThat(
|
||||||
@@ -86,8 +86,8 @@ public class ScmModuleTest
|
|||||||
assertThat(
|
assertThat(
|
||||||
module.getEvents(),
|
module.getEvents(),
|
||||||
containsInAnyOrder(
|
containsInAnyOrder(
|
||||||
(Class<?>) String.class,
|
String.class,
|
||||||
(Class<?>) Boolean.class
|
Boolean.class
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertThat(
|
assertThat(
|
||||||
@@ -100,15 +100,15 @@ public class ScmModuleTest
|
|||||||
assertThat(
|
assertThat(
|
||||||
module.getRestProviders(),
|
module.getRestProviders(),
|
||||||
containsInAnyOrder(
|
containsInAnyOrder(
|
||||||
(Class<?>) Integer.class,
|
Integer.class,
|
||||||
(Class<?>) Long.class
|
Long.class
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertThat(
|
assertThat(
|
||||||
module.getRestResources(),
|
module.getRestResources(),
|
||||||
containsInAnyOrder(
|
containsInAnyOrder(
|
||||||
(Class<?>) Float.class,
|
Float.class,
|
||||||
(Class<?>) Double.class
|
Double.class
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
//J+
|
//J+
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ public class TemplateEngineFactoryTest
|
|||||||
assertTrue(engines.contains(engine1));
|
assertTrue(engines.contains(engine1));
|
||||||
assertTrue(engines.contains(engine2));
|
assertTrue(engines.contains(engine2));
|
||||||
|
|
||||||
Set<TemplateEngine> ce = new HashSet<TemplateEngine>();
|
Set<TemplateEngine> ce = new HashSet<>();
|
||||||
|
|
||||||
ce.add(engine1);
|
ce.add(engine1);
|
||||||
factory = new TemplateEngineFactory(ce, engine2);
|
factory = new TemplateEngineFactory(ce, engine2);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class UrlBuilderTest
|
|||||||
UrlBuilder builder = new UrlBuilder("http://www.short.de");
|
UrlBuilder builder = new UrlBuilder("http://www.short.de");
|
||||||
|
|
||||||
builder.appendParameter("i", 123).appendParameter("s", "abc");
|
builder.appendParameter("i", 123).appendParameter("s", "abc");
|
||||||
builder.appendParameter("b", true).appendParameter("l", 321l);
|
builder.appendParameter("b", true).appendParameter("l", 321L);
|
||||||
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString());
|
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString());
|
||||||
builder.appendParameter("c", "a b");
|
builder.appendParameter("c", "a b");
|
||||||
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString());
|
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString());
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ public class XmlGroupDatabase implements XmlDatabase<Group>
|
|||||||
/** Field description */
|
/** Field description */
|
||||||
@XmlJavaTypeAdapter(XmlGroupMapAdapter.class)
|
@XmlJavaTypeAdapter(XmlGroupMapAdapter.class)
|
||||||
@XmlElement(name = "groups")
|
@XmlElement(name = "groups")
|
||||||
private Map<String, Group> groupMap = new LinkedHashMap<String, Group>();
|
private Map<String, Group> groupMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private Long lastModified;
|
private Long lastModified;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class XmlGroupList implements Iterable<Group>
|
|||||||
*/
|
*/
|
||||||
public XmlGroupList(Map<String, Group> groupMap)
|
public XmlGroupList(Map<String, Group> groupMap)
|
||||||
{
|
{
|
||||||
this.groups = new LinkedList<Group>(groupMap.values());
|
this.groups = new LinkedList<>(groupMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class XmlGroupMapAdapter
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, Group> unmarshal(XmlGroupList groups) throws Exception
|
public Map<String, Group> unmarshal(XmlGroupList groups) throws Exception
|
||||||
{
|
{
|
||||||
Map<String, Group> groupMap = new LinkedHashMap<String, Group>();
|
Map<String, Group> groupMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (Group group : groups)
|
for (Group group : groups)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import sonia.scm.ContextEntry;
|
import sonia.scm.ContextEntry;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.store.CopyOnWrite;
|
||||||
import sonia.scm.store.StoreConstants;
|
import sonia.scm.store.StoreConstants;
|
||||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||||
|
|
||||||
@@ -43,7 +44,10 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
|||||||
try {
|
try {
|
||||||
Marshaller marshaller = jaxbContext.createMarshaller();
|
Marshaller marshaller = jaxbContext.createMarshaller();
|
||||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||||
marshaller.marshal(repository, resolveDataPath(path).toFile());
|
CopyOnWrite.withTemporaryFile(
|
||||||
|
temp -> marshaller.marshal(repository, temp.toFile()),
|
||||||
|
resolveDataPath(path)
|
||||||
|
);
|
||||||
} catch (JAXBException ex) {
|
} catch (JAXBException ex) {
|
||||||
throw new InternalRepositoryException(repository, "failed write repository metadata", ex);
|
throw new InternalRepositoryException(repository, "failed write repository metadata", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.ContextEntry;
|
import sonia.scm.ContextEntry;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
import sonia.scm.store.CopyOnWrite;
|
||||||
import sonia.scm.xml.IndentXMLStreamWriter;
|
import sonia.scm.xml.IndentXMLStreamWriter;
|
||||||
import sonia.scm.xml.XmlStreams;
|
import sonia.scm.xml.XmlStreams;
|
||||||
|
|
||||||
@@ -40,23 +41,28 @@ class PathDatabase {
|
|||||||
ensureParentDirectoryExists();
|
ensureParentDirectoryExists();
|
||||||
LOG.trace("write repository path database to {}", storePath);
|
LOG.trace("write repository path database to {}", storePath);
|
||||||
|
|
||||||
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(storePath)) {
|
CopyOnWrite.withTemporaryFile(
|
||||||
writer.writeStartDocument(ENCODING, VERSION);
|
temp -> {
|
||||||
|
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) {
|
||||||
|
writer.writeStartDocument(ENCODING, VERSION);
|
||||||
|
|
||||||
writeRepositoriesStart(writer, creationTime, lastModified);
|
writeRepositoriesStart(writer, creationTime, lastModified);
|
||||||
for (Map.Entry<String, Path> e : pathDatabase.entrySet()) {
|
for (Map.Entry<String, Path> e : pathDatabase.entrySet()) {
|
||||||
writeRepository(writer, e.getKey(), e.getValue());
|
writeRepository(writer, e.getKey(), e.getValue());
|
||||||
}
|
}
|
||||||
writer.writeEndElement();
|
writer.writeEndElement();
|
||||||
|
|
||||||
writer.writeEndDocument();
|
writer.writeEndDocument();
|
||||||
} catch (XMLStreamException | IOException ex) {
|
} catch (XMLStreamException | IOException ex) {
|
||||||
throw new InternalRepositoryException(
|
throw new InternalRepositoryException(
|
||||||
ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
|
ContextEntry.ContextBuilder.entity(Path.class, storePath.toString()).build(),
|
||||||
"failed to write repository path database",
|
"failed to write repository path database",
|
||||||
ex
|
ex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
storePath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureParentDirectoryExists() {
|
private void ensureParentDirectoryExists() {
|
||||||
|
|||||||
112
scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java
Normal file
112
scm-dao-xml/src/main/java/sonia/scm/store/CopyOnWrite.java
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class CopyOnWrite {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(CopyOnWrite.class);
|
||||||
|
|
||||||
|
private CopyOnWrite() {}
|
||||||
|
|
||||||
|
public static void withTemporaryFile(FileWriter writer, Path targetFile) {
|
||||||
|
validateInput(targetFile);
|
||||||
|
Path temporaryFile = createTemporaryFile(targetFile);
|
||||||
|
executeCallback(writer, targetFile, temporaryFile);
|
||||||
|
replaceOriginalFile(targetFile, temporaryFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("squid:S3725") // performance of Files#isDirectory
|
||||||
|
private static void validateInput(Path targetFile) {
|
||||||
|
if (Files.isDirectory(targetFile)) {
|
||||||
|
throw new IllegalArgumentException("target file has to be a regular file, not a directory");
|
||||||
|
}
|
||||||
|
if (targetFile.getParent() == null) {
|
||||||
|
throw new IllegalArgumentException("target file has to be specified with a parent directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path createTemporaryFile(Path targetFile) {
|
||||||
|
Path temporaryFile = targetFile.getParent().resolve(UUID.randomUUID().toString());
|
||||||
|
try {
|
||||||
|
Files.createFile(temporaryFile);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LOG.error("Error creating temporary file {} to replace file {}", temporaryFile, targetFile);
|
||||||
|
throw new StoreException("could not create temporary file", ex);
|
||||||
|
}
|
||||||
|
return temporaryFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void executeCallback(FileWriter writer, Path targetFile, Path temporaryFile) {
|
||||||
|
try {
|
||||||
|
writer.write(temporaryFile);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.error("Error writing to temporary file {}. Target file {} has not been modified", temporaryFile, targetFile);
|
||||||
|
throw new StoreException("could not write temporary file", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void replaceOriginalFile(Path targetFile, Path temporaryFile) {
|
||||||
|
Path backupFile = backupOriginalFile(targetFile);
|
||||||
|
try {
|
||||||
|
Files.move(temporaryFile, targetFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Error renaming temporary file {} to target file {}", temporaryFile, targetFile);
|
||||||
|
restoreBackup(targetFile, backupFile);
|
||||||
|
throw new StoreException("could rename temporary file to target file", e);
|
||||||
|
}
|
||||||
|
deleteBackupFile(backupFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("squid:S3725") // performance of Files#exists
|
||||||
|
private static Path backupOriginalFile(Path targetFile) {
|
||||||
|
Path directory = targetFile.getParent();
|
||||||
|
if (Files.exists(targetFile)) {
|
||||||
|
Path backupFile = directory.resolve(UUID.randomUUID().toString());
|
||||||
|
try {
|
||||||
|
Files.move(targetFile, backupFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Could not backup original file {}. Aborting here so that original file will not be overwritten.", targetFile);
|
||||||
|
throw new StoreException("could not create backup of file", e);
|
||||||
|
}
|
||||||
|
return backupFile;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteBackupFile(Path backupFile) {
|
||||||
|
if (backupFile != null) {
|
||||||
|
try {
|
||||||
|
Files.delete(backupFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warn("Could not delete backup file {}", backupFile);
|
||||||
|
throw new StoreException("could not delete backup file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void restoreBackup(Path targetFile, Path backupFile) {
|
||||||
|
if (backupFile != null) {
|
||||||
|
try {
|
||||||
|
Files.move(backupFile, targetFile);
|
||||||
|
LOG.info("Recovered original file {} from backup", targetFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Could not replace original file {} with backup file {} after failure", targetFile, backupFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface FileWriter {
|
||||||
|
@SuppressWarnings("squid:S00112") // We do not want to limit exceptions here
|
||||||
|
void write(Path t) throws Exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,48 +317,47 @@ public class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V
|
|||||||
{
|
{
|
||||||
logger.debug("store configuration to {}", file);
|
logger.debug("store configuration to {}", file);
|
||||||
|
|
||||||
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(file))
|
CopyOnWrite.withTemporaryFile(
|
||||||
{
|
temp -> {
|
||||||
writer.writeStartDocument();
|
try (IndentXMLStreamWriter writer = XmlStreams.createWriter(temp)) {
|
||||||
|
writer.writeStartDocument();
|
||||||
|
|
||||||
// configuration start
|
// configuration start
|
||||||
writer.writeStartElement(TAG_CONFIGURATION);
|
writer.writeStartElement(TAG_CONFIGURATION);
|
||||||
|
|
||||||
Marshaller m = context.createMarshaller();
|
Marshaller m = context.createMarshaller();
|
||||||
|
|
||||||
m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
|
m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
|
||||||
|
|
||||||
for (Entry<String, V> e : entries.entrySet())
|
for (Entry<String, V> e : entries.entrySet()) {
|
||||||
{
|
|
||||||
|
|
||||||
// entry start
|
// entry start
|
||||||
writer.writeStartElement(TAG_ENTRY);
|
writer.writeStartElement(TAG_ENTRY);
|
||||||
|
|
||||||
// key start
|
// key start
|
||||||
writer.writeStartElement(TAG_KEY);
|
writer.writeStartElement(TAG_KEY);
|
||||||
writer.writeCharacters(e.getKey());
|
writer.writeCharacters(e.getKey());
|
||||||
|
|
||||||
// key end
|
// key end
|
||||||
writer.writeEndElement();
|
writer.writeEndElement();
|
||||||
|
|
||||||
// value
|
// value
|
||||||
JAXBElement<V> je = new JAXBElement<V>(QName.valueOf(TAG_VALUE), type,
|
JAXBElement<V> je = new JAXBElement<>(QName.valueOf(TAG_VALUE), type,
|
||||||
e.getValue());
|
e.getValue());
|
||||||
|
|
||||||
m.marshal(je, writer);
|
m.marshal(je, writer);
|
||||||
|
|
||||||
// entry end
|
// entry end
|
||||||
writer.writeEndElement();
|
writer.writeEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
// configuration end
|
// configuration end
|
||||||
writer.writeEndElement();
|
writer.writeEndElement();
|
||||||
writer.writeEndDocument();
|
writer.writeEndDocument();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
},
|
||||||
{
|
file.toPath()
|
||||||
throw new StoreException("could not store configuration", ex);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
|
|||||||
Marshaller marshaller = context.createMarshaller();
|
Marshaller marshaller = context.createMarshaller();
|
||||||
|
|
||||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||||
marshaller.marshal(object, configFile);
|
CopyOnWrite.withTemporaryFile(
|
||||||
|
temp -> marshaller.marshal(object, temp.toFile()),
|
||||||
|
configFile.toPath()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (JAXBException ex) {
|
catch (JAXBException ex) {
|
||||||
throw new StoreException("failed to marshall object", ex);
|
throw new StoreException("failed to marshall object", ex);
|
||||||
|
|||||||
@@ -202,5 +202,5 @@ public class XmlUserDatabase implements XmlDatabase<User>
|
|||||||
/** Field description */
|
/** Field description */
|
||||||
@XmlJavaTypeAdapter(XmlUserMapAdapter.class)
|
@XmlJavaTypeAdapter(XmlUserMapAdapter.class)
|
||||||
@XmlElement(name = "users")
|
@XmlElement(name = "users")
|
||||||
private Map<String, User> userMap = new LinkedHashMap<String, User>();
|
private Map<String, User> userMap = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class XmlUserList implements Iterable<User>
|
|||||||
*/
|
*/
|
||||||
public XmlUserList(Map<String, User> userMap)
|
public XmlUserList(Map<String, User> userMap)
|
||||||
{
|
{
|
||||||
this.users = new LinkedList<User>(userMap.values());
|
this.users = new LinkedList<>(userMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class XmlUserMapAdapter
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, User> unmarshal(XmlUserList users) throws Exception
|
public Map<String, User> unmarshal(XmlUserList users) throws Exception
|
||||||
{
|
{
|
||||||
Map<String, User> userMap = new LinkedHashMap<String, User>();
|
Map<String, User> userMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (User user : users)
|
for (User user : users)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ public final class XmlStreams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static XMLStreamReader createReader(Reader reader) throws XMLStreamException {
|
private static XMLStreamReader createReader(Reader reader) throws XMLStreamException {
|
||||||
return XMLInputFactory.newInstance().createXMLStreamReader(reader);
|
XMLInputFactory factory = XMLInputFactory.newInstance();
|
||||||
|
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
|
||||||
|
return factory.createXMLStreamReader(reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
114
scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java
Normal file
114
scm-dao-xml/src/test/java/sonia/scm/store/CopyOnWriteTest.java
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package sonia.scm.store;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
|
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static sonia.scm.store.CopyOnWrite.withTemporaryFile;
|
||||||
|
|
||||||
|
@ExtendWith(TempDirectory.class)
|
||||||
|
class CopyOnWriteTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateNewFile(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
Path expectedFile = tempDir.resolve("toBeCreated.txt");
|
||||||
|
|
||||||
|
withTemporaryFile(
|
||||||
|
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
|
||||||
|
expectedFile);
|
||||||
|
|
||||||
|
Assertions.assertThat(expectedFile).hasContent("great success");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldOverwriteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path expectedFile = tempDir.resolve("toBeOverwritten.txt");
|
||||||
|
Files.createFile(expectedFile);
|
||||||
|
|
||||||
|
withTemporaryFile(
|
||||||
|
file -> new FileOutputStream(file.toFile()).write("great success".getBytes()),
|
||||||
|
expectedFile);
|
||||||
|
|
||||||
|
Assertions.assertThat(expectedFile).hasContent("great success");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailForDirectory(@TempDirectory.TempDir Path tempDir) {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> withTemporaryFile(
|
||||||
|
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
|
||||||
|
tempDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailForMissingDirectory() {
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> withTemporaryFile(
|
||||||
|
file -> new FileOutputStream(file.toFile()).write("should not be written".getBytes()),
|
||||||
|
Paths.get("someFile")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path unchangedOriginalFile = tempDir.resolve("notToBeDeleted.txt");
|
||||||
|
new FileOutputStream(unchangedOriginalFile.toFile()).write("this should be kept".getBytes());
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
StoreException.class,
|
||||||
|
() -> withTemporaryFile(
|
||||||
|
file -> {
|
||||||
|
throw new IOException("test");
|
||||||
|
},
|
||||||
|
unchangedOriginalFile));
|
||||||
|
|
||||||
|
Assertions.assertThat(unchangedOriginalFile).hasContent("this should be kept");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotWrapRuntimeExceptions(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path someFile = tempDir.resolve("something.txt");
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
() -> withTemporaryFile(
|
||||||
|
file -> {
|
||||||
|
throw new NullPointerException("test");
|
||||||
|
},
|
||||||
|
someFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path backedUpFile = tempDir.resolve("notToBeDeleted.txt");
|
||||||
|
new FileOutputStream(backedUpFile.toFile()).write("this should be kept".getBytes());
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
StoreException.class,
|
||||||
|
() -> withTemporaryFile(
|
||||||
|
Files::delete,
|
||||||
|
backedUpFile));
|
||||||
|
|
||||||
|
Assertions.assertThat(backedUpFile).hasContent("this should be kept");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteExistingFile(@TempDirectory.TempDir Path tempDir) throws IOException {
|
||||||
|
Path expectedFile = tempDir.resolve("toBeReplaced.txt");
|
||||||
|
new FileOutputStream(expectedFile.toFile()).write("this should be removed".getBytes());
|
||||||
|
|
||||||
|
withTemporaryFile(
|
||||||
|
file -> new FileOutputStream(file.toFile()).write("overwritten".getBytes()),
|
||||||
|
expectedFile);
|
||||||
|
|
||||||
|
Assertions.assertThat(Files.list(tempDir)).hasSize(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,13 @@
|
|||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.resteasy</groupId>
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
<artifactId>resteasy-jaxrs</artifactId>
|
<artifactId>resteasy-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-core-spi</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
import static sonia.scm.NotFoundException.notFound;
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
@@ -209,7 +210,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
|||||||
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
||||||
RevCommit commit = getLatestCommit(repo, revId, path);
|
RevCommit commit = getLatestCommit(repo, revId, path);
|
||||||
|
|
||||||
Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, commit, treeWalk);
|
Optional<LfsPointer> lfsPointer = commit == null? empty(): GitUtil.getLfsPointer(repo, path, commit, treeWalk);
|
||||||
|
|
||||||
if (lfsPointer.isPresent()) {
|
if (lfsPointer.isPresent()) {
|
||||||
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ const Switcher = styled(ButtonAddons)`
|
|||||||
right: 0;
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const SmallButton = styled(Button).attrs(props => ({
|
||||||
|
className: "is-small"
|
||||||
|
}))`
|
||||||
|
height: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
};
|
};
|
||||||
@@ -62,9 +68,9 @@ export default class ProtocolInformation extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button color={color} action={() => this.selectProtocol(protocol)}>
|
<SmallButton color={color} action={() => this.selectProtocol(protocol)}>
|
||||||
{name.toUpperCase()}
|
{name.toUpperCase()}
|
||||||
</Button>
|
</SmallButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { FormEvent } from "react";
|
import React, { FormEvent } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
import { Branch, Repository, Link } from "@scm-manager/ui-types";
|
import { Branch, Repository, Link } from "@scm-manager/ui-types";
|
||||||
import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton } from "@scm-manager/ui-components";
|
import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, Level, SubmitButton } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -143,10 +143,14 @@ class RepositoryConfig extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitButton = disabled ? null : (
|
const submitButton = disabled ? null : (
|
||||||
<SubmitButton
|
<Level
|
||||||
label={t("scm-git-plugin.repo-config.submit")}
|
right={
|
||||||
loading={submitPending}
|
<SubmitButton
|
||||||
disabled={!this.state.selectedBranchName}
|
label={t("scm-git-plugin.repo-config.submit")}
|
||||||
|
loading={submitPending}
|
||||||
|
disabled={!this.state.selectedBranchName}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ package sonia.scm.api.v2.resources;
|
|||||||
|
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
@@ -27,6 +24,7 @@ import sonia.scm.repository.RepositoryManager;
|
|||||||
import sonia.scm.store.ConfigurationStore;
|
import sonia.scm.store.ConfigurationStore;
|
||||||
import sonia.scm.store.ConfigurationStoreFactory;
|
import sonia.scm.store.ConfigurationStoreFactory;
|
||||||
import sonia.scm.web.GitVndMediaType;
|
import sonia.scm.web.GitVndMediaType;
|
||||||
|
import sonia.scm.web.RestDispatcher;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@@ -52,10 +50,7 @@ public class GitConfigResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
@Rule
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
|
||||||
|
|
||||||
private final URI baseUri = URI.create("/");
|
private final URI baseUri = URI.create("/");
|
||||||
|
|
||||||
@@ -89,7 +84,7 @@ public class GitConfigResourceTest {
|
|||||||
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
||||||
GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory));
|
GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigMapper, repositoryManager, new GitRepositoryConfigStoreProvider(configurationStoreFactory));
|
||||||
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource));
|
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource));
|
||||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
dispatcher.addSingletonResource(gitConfigResource);
|
||||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +132,11 @@ public class GitConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "writeOnly")
|
@SubjectAware(username = "writeOnly")
|
||||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:read:git]");
|
MockHttpResponse response = get();
|
||||||
|
|
||||||
get();
|
assertEquals("Subject does not have permission [configuration:read:git]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -152,10 +148,11 @@ public class GitConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:git]");
|
MockHttpResponse response = put();
|
||||||
|
|
||||||
put();
|
assertEquals("Subject does not have permission [configuration:write:git]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public class HgPackageReader
|
|||||||
*/
|
*/
|
||||||
private void filterPackage(HgPackages packages)
|
private void filterPackage(HgPackages packages)
|
||||||
{
|
{
|
||||||
List<HgPackage> pkgList = new ArrayList<HgPackage>();
|
List<HgPackage> pkgList = new ArrayList<>();
|
||||||
|
|
||||||
for (HgPackage pkg : packages)
|
for (HgPackage pkg : packages)
|
||||||
{
|
{
|
||||||
@@ -228,7 +228,7 @@ public class HgPackageReader
|
|||||||
if (packages == null)
|
if (packages == null)
|
||||||
{
|
{
|
||||||
packages = new HgPackages();
|
packages = new HgPackages();
|
||||||
packages.setPackages(new ArrayList<HgPackage>());
|
packages.setPackages(new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ package sonia.scm.api.v2.resources;
|
|||||||
|
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
@@ -18,12 +15,15 @@ import org.mockito.junit.MockitoJUnitRunner;
|
|||||||
import sonia.scm.repository.HgConfig;
|
import sonia.scm.repository.HgConfig;
|
||||||
import sonia.scm.repository.HgRepositoryHandler;
|
import sonia.scm.repository.HgRepositoryHandler;
|
||||||
import sonia.scm.web.HgVndMediaType;
|
import sonia.scm.web.HgVndMediaType;
|
||||||
|
import sonia.scm.web.RestDispatcher;
|
||||||
|
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -37,10 +37,7 @@ public class HgConfigAutoConfigurationResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
@Rule
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper;
|
private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper;
|
||||||
@@ -57,7 +54,7 @@ public class HgConfigAutoConfigurationResourceTest {
|
|||||||
new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler);
|
new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler);
|
||||||
|
|
||||||
when(resourceProvider.get()).thenReturn(resource);
|
when(resourceProvider.get()).thenReturn(resource);
|
||||||
dispatcher.getRegistry().addSingletonResource(
|
dispatcher.addSingletonResource(
|
||||||
new HgConfigResource(null, null, null, null,
|
new HgConfigResource(null, null, null, null,
|
||||||
resourceProvider, null));
|
resourceProvider, null));
|
||||||
}
|
}
|
||||||
@@ -76,9 +73,10 @@ public class HgConfigAutoConfigurationResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception {
|
public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
MockHttpResponse response = put(null);
|
||||||
|
|
||||||
put(null);
|
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -95,9 +93,10 @@ public class HgConfigAutoConfigurationResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception {
|
public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
MockHttpResponse response = put("{\"disabled\":true}");
|
||||||
|
|
||||||
put("{\"disabled\":true}");
|
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MockHttpResponse put(String content) throws URISyntaxException {
|
private MockHttpResponse put(String content) throws URISyntaxException {
|
||||||
|
|||||||
@@ -2,19 +2,17 @@ package sonia.scm.api.v2.resources;
|
|||||||
|
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import sonia.scm.web.RestDispatcher;
|
||||||
|
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -35,10 +33,7 @@ public class HgConfigInstallationsResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
@Rule
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
|
||||||
|
|
||||||
private final URI baseUri = URI.create("/");
|
private final URI baseUri = URI.create("/");
|
||||||
|
|
||||||
@@ -57,7 +52,7 @@ public class HgConfigInstallationsResourceTest {
|
|||||||
HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper);
|
HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper);
|
||||||
|
|
||||||
when(resourceProvider.get()).thenReturn(resource);
|
when(resourceProvider.get()).thenReturn(resource);
|
||||||
dispatcher.getRegistry().addSingletonResource(
|
dispatcher.addSingletonResource(
|
||||||
new HgConfigResource(null, null, null, null,
|
new HgConfigResource(null, null, null, null,
|
||||||
null, resourceProvider));
|
null, resourceProvider));
|
||||||
|
|
||||||
@@ -82,9 +77,10 @@ public class HgConfigInstallationsResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "writeOnly")
|
@SubjectAware(username = "writeOnly")
|
||||||
public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception {
|
public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
MockHttpResponse response = get("hg");
|
||||||
|
|
||||||
get("hg");
|
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -104,9 +100,10 @@ public class HgConfigInstallationsResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "writeOnly")
|
@SubjectAware(username = "writeOnly")
|
||||||
public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception {
|
public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
MockHttpResponse response = get("python");
|
||||||
|
|
||||||
get("python");
|
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MockHttpResponse get(String path) throws URISyntaxException {
|
private MockHttpResponse get(String path) throws URISyntaxException {
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
@@ -23,6 +20,7 @@ import sonia.scm.installer.HgPackageReader;
|
|||||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||||
import sonia.scm.repository.HgConfig;
|
import sonia.scm.repository.HgConfig;
|
||||||
import sonia.scm.repository.HgRepositoryHandler;
|
import sonia.scm.repository.HgRepositoryHandler;
|
||||||
|
import sonia.scm.web.RestDispatcher;
|
||||||
|
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -49,10 +47,7 @@ public class HgConfigPackageResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
@Rule
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
|
||||||
|
|
||||||
private final URI baseUri = java.net.URI.create("/");
|
private final URI baseUri = java.net.URI.create("/");
|
||||||
|
|
||||||
@@ -113,9 +108,10 @@ public class HgConfigPackageResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "writeOnly")
|
@SubjectAware(username = "writeOnly")
|
||||||
public void shouldNotGetPackagesWhenNotAuthorized() throws Exception {
|
public void shouldNotGetPackagesWhenNotAuthorized() throws Exception {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
MockHttpResponse response = get();
|
||||||
|
|
||||||
get();
|
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -158,9 +154,10 @@ public class HgConfigPackageResourceTest {
|
|||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotInstallPackageWhenNotAuthorized() throws Exception {
|
public void shouldNotInstallPackageWhenNotAuthorized() throws Exception {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
MockHttpResponse response = put("don-t-care");
|
||||||
|
|
||||||
put("don-t-care");
|
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HgPackage> createPackages() {
|
private List<HgPackage> createPackages() {
|
||||||
@@ -191,7 +188,7 @@ public class HgConfigPackageResourceTest {
|
|||||||
new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper);
|
new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper);
|
||||||
|
|
||||||
when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource);
|
when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource);
|
||||||
dispatcher.getRegistry().addSingletonResource(
|
dispatcher.addSingletonResource(
|
||||||
new HgConfigResource(null, null, null,
|
new HgConfigResource(null, null, null,
|
||||||
hgConfigPackageResourceProvider, null, null));
|
hgConfigPackageResourceProvider, null, null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
@@ -20,6 +17,7 @@ import org.mockito.junit.MockitoJUnitRunner;
|
|||||||
import sonia.scm.repository.HgConfig;
|
import sonia.scm.repository.HgConfig;
|
||||||
import sonia.scm.repository.HgRepositoryHandler;
|
import sonia.scm.repository.HgRepositoryHandler;
|
||||||
import sonia.scm.web.HgVndMediaType;
|
import sonia.scm.web.HgVndMediaType;
|
||||||
|
import sonia.scm.web.RestDispatcher;
|
||||||
|
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -43,10 +41,7 @@ public class HgConfigResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
@Rule
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
|
||||||
|
|
||||||
private final URI baseUri = URI.create("/");
|
private final URI baseUri = URI.create("/");
|
||||||
|
|
||||||
@@ -78,7 +73,7 @@ public class HgConfigResourceTest {
|
|||||||
HgConfigResource gitConfigResource =
|
HgConfigResource gitConfigResource =
|
||||||
new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource,
|
new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource,
|
||||||
autoconfigResource, installationsResource);
|
autoconfigResource, installationsResource);
|
||||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
dispatcher.addSingletonResource(gitConfigResource);
|
||||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,10 +115,11 @@ public class HgConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "writeOnly")
|
@SubjectAware(username = "writeOnly")
|
||||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:read:hg]");
|
MockHttpResponse response = get();
|
||||||
|
|
||||||
get();
|
assertEquals("Subject does not have permission [configuration:read:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -135,10 +131,11 @@ public class HgConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:hg]");
|
MockHttpResponse response = put();
|
||||||
|
|
||||||
put();
|
assertEquals("Subject does not have permission [configuration:write:hg]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MockHttpResponse get() throws URISyntaxException {
|
private MockHttpResponse get() throws URISyntaxException {
|
||||||
|
|||||||
@@ -211,15 +211,7 @@ public class SVNKitLogger extends SVNDebugLogAdapter
|
|||||||
*/
|
*/
|
||||||
private Logger getLogger(SVNLogType type)
|
private Logger getLogger(SVNLogType type)
|
||||||
{
|
{
|
||||||
Logger logger = loggerMap.get(type);
|
return loggerMap.computeIfAbsent(type, t -> LoggerFactory.getLogger(parseName(t.getName())));
|
||||||
|
|
||||||
if (logger == null)
|
|
||||||
{
|
|
||||||
logger = LoggerFactory.getLogger(parseName(type.getName()));
|
|
||||||
loggerMap.put(type, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
return logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.jboss.resteasy.core.Dispatcher;
|
|
||||||
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|
||||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
@@ -19,6 +16,7 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
import sonia.scm.repository.SvnConfig;
|
import sonia.scm.repository.SvnConfig;
|
||||||
import sonia.scm.repository.SvnRepositoryHandler;
|
import sonia.scm.repository.SvnRepositoryHandler;
|
||||||
|
import sonia.scm.web.RestDispatcher;
|
||||||
import sonia.scm.web.SvnVndMediaType;
|
import sonia.scm.web.SvnVndMediaType;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@@ -42,10 +40,7 @@ public class SvnConfigResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
@Rule
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
public ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
|
||||||
|
|
||||||
private final URI baseUri = URI.create("/");
|
private final URI baseUri = URI.create("/");
|
||||||
|
|
||||||
@@ -66,7 +61,7 @@ public class SvnConfigResourceTest {
|
|||||||
SvnConfig gitConfig = createConfiguration();
|
SvnConfig gitConfig = createConfiguration();
|
||||||
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
|
||||||
SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler);
|
SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler);
|
||||||
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
|
dispatcher.addSingletonResource(gitConfigResource);
|
||||||
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +103,11 @@ public class SvnConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "writeOnly")
|
@SubjectAware(username = "writeOnly")
|
||||||
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException {
|
public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:read:svn]");
|
MockHttpResponse response = get();
|
||||||
|
|
||||||
get();
|
assertEquals("Subject does not have permission [configuration:read:svn]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -123,10 +119,11 @@ public class SvnConfigResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readOnly")
|
@SubjectAware(username = "readOnly")
|
||||||
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
|
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
thrown.expectMessage("Subject does not have permission [configuration:write:svn]");
|
MockHttpResponse response = put();
|
||||||
|
|
||||||
put();
|
assertEquals("Subject does not have permission [configuration:write:svn]", response.getContentAsString());
|
||||||
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
private MockHttpResponse get() throws URISyntaxException {
|
private MockHttpResponse get() throws URISyntaxException {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
<systemProperties>
|
<systemProperties>
|
||||||
<arg>java.awt.headless=true</arg>
|
<arg>java.awt.headless=true</arg>
|
||||||
<arg>logback.configurationFile=logging.xml</arg>
|
<arg>logback.configurationFile=logging.xml</arg>
|
||||||
|
<arg>ClassLoaderLeakPreventor.threadWaitMs=100</arg>
|
||||||
</systemProperties>
|
</systemProperties>
|
||||||
</jvmSettings>
|
</jvmSettings>
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,20 @@
|
|||||||
<version>${mockito.version}</version>
|
<version>${mockito.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-core-spi</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||||
|
<version>${resteasy.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-simple</artifactId>
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
|||||||
@@ -81,15 +81,7 @@ public class MapCacheManager
|
|||||||
@Override
|
@Override
|
||||||
public synchronized <K, V> MapCache<K, V> getCache(String name)
|
public synchronized <K, V> MapCache<K, V> getCache(String name)
|
||||||
{
|
{
|
||||||
MapCache<K, V> cache = cacheMap.get(name);
|
return (MapCache<K, V>) cacheMap.computeIfAbsent(name, k -> new MapCache<K, V>());
|
||||||
|
|
||||||
if (cache == null)
|
|
||||||
{
|
|
||||||
cache = new MapCache<K, V>();
|
|
||||||
cacheMap.put(name, cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|||||||
111
scm-test/src/main/java/sonia/scm/web/RestDispatcher.java
Normal file
111
scm-test/src/main/java/sonia/scm/web/RestDispatcher.java
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package sonia.scm.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
|
import org.apache.shiro.authz.UnauthorizedException;
|
||||||
|
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||||
|
import org.jboss.resteasy.spi.Dispatcher;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.AlreadyExistsException;
|
||||||
|
import sonia.scm.BadRequestException;
|
||||||
|
import sonia.scm.ConcurrentModificationException;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
|
import sonia.scm.ScmConstraintViolationException;
|
||||||
|
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
import javax.ws.rs.ext.ContextResolver;
|
||||||
|
import javax.ws.rs.ext.ExceptionMapper;
|
||||||
|
import javax.ws.rs.ext.Provider;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class RestDispatcher {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(RestDispatcher.class);
|
||||||
|
|
||||||
|
private final Dispatcher dispatcher;
|
||||||
|
private final EnhanceableExceptionMapper exceptionMapper;
|
||||||
|
|
||||||
|
public RestDispatcher() {
|
||||||
|
dispatcher = MockDispatcherFactory.createDispatcher();
|
||||||
|
exceptionMapper = new EnhanceableExceptionMapper();
|
||||||
|
dispatcher.getProviderFactory().register(exceptionMapper);
|
||||||
|
dispatcher.getProviderFactory().registerProviderInstance(new JacksonProducer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSingletonResource(Object resource) {
|
||||||
|
dispatcher.getRegistry().addSingletonResource(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invoke(HttpRequest in, HttpResponse response) {
|
||||||
|
dispatcher.invoke(in, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerException(Class<? extends RuntimeException> exceptionClass, Status status) {
|
||||||
|
exceptionMapper.registerException(exceptionClass, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putDefaultContextObject(Class<T> clazz, T object) {
|
||||||
|
dispatcher.getDefaultContextObjects().put(clazz, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EnhanceableExceptionMapper implements ExceptionMapper<Exception> {
|
||||||
|
|
||||||
|
private final Map<Class<? extends RuntimeException>, Integer> statusCodes = new HashMap<>();
|
||||||
|
|
||||||
|
public EnhanceableExceptionMapper() {
|
||||||
|
registerException(NotFoundException.class, Status.NOT_FOUND);
|
||||||
|
registerException(AlreadyExistsException.class, Status.CONFLICT);
|
||||||
|
registerException(ConcurrentModificationException.class, Status.CONFLICT);
|
||||||
|
registerException(UnauthorizedException.class, Status.FORBIDDEN);
|
||||||
|
registerException(AuthorizationException.class, Status.FORBIDDEN);
|
||||||
|
registerException(BadRequestException.class, Status.BAD_REQUEST);
|
||||||
|
registerException(ScmConstraintViolationException.class, Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerException(Class<? extends RuntimeException> exceptionClass, Status status) {
|
||||||
|
statusCodes.put(exceptionClass, status.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Exception e) {
|
||||||
|
return Response.status(getStatus(e)).entity(e.getMessage()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer getStatus(Exception ex) {
|
||||||
|
return statusCodes
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.filter(e -> e.getKey().isAssignableFrom(ex.getClass()))
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.findAny()
|
||||||
|
.orElse(handleUnknownException(ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer handleUnknownException(Exception ex) {
|
||||||
|
LOG.info("got unknown exception in rest api test", ex);
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
@Produces("application/*+json")
|
||||||
|
public static class JacksonProducer implements ContextResolver<ObjectMapper> {
|
||||||
|
public JacksonProducer() {
|
||||||
|
this.json
|
||||||
|
= new ObjectMapper().findAndRegisterModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ObjectMapper getContext(Class<?> objectType) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ObjectMapper json;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
<name>scm-ui</name>
|
<name>scm-ui</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<build.script>build</build.script>
|
||||||
|
<skipTests>false</skipTests>
|
||||||
<sonar.language>typescript</sonar.language>
|
<sonar.language>typescript</sonar.language>
|
||||||
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
|
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
|
||||||
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
|
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
<goal>run</goal>
|
<goal>run</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<script>build</script>
|
<script>${build.script}</script>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
<execution>
|
<execution>
|
||||||
@@ -118,4 +120,21 @@
|
|||||||
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>dev</id>
|
||||||
|
|
||||||
|
<activation>
|
||||||
|
<property>
|
||||||
|
<name>development</name>
|
||||||
|
</property>
|
||||||
|
</activation>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<build.script>build:dev</build.script>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
|
||||||
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"private": false,
|
"private": false,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier": "^1.18.2"
|
"prettier": "^1.19.1"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"private": false,
|
"private": false,
|
||||||
"main": "tsconfig.json",
|
"main": "tsconfig.json",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"typescript": "^3.6.4"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"raf": "^3.4.0",
|
"raf": "^3.4.0",
|
||||||
"react-test-renderer": "^16.10.2",
|
"react-test-renderer": "^16.10.2",
|
||||||
"storybook-addon-i18next": "^1.2.1",
|
"storybook-addon-i18next": "^1.2.1",
|
||||||
"typescript": "^3.6.4"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",
|
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
import { Async, AsyncCreatable } from "react-select";
|
import { Async, AsyncCreatable } from "react-select";
|
||||||
import { SelectValue } from "@scm-manager/ui-types";
|
import { SelectValue } from "@scm-manager/ui-types";
|
||||||
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
|
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
|
||||||
@@ -7,13 +8,14 @@ import { ActionMeta, ValueType } from "react-select/lib/types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
loadSuggestions: (p: string) => Promise<SelectValue[]>;
|
loadSuggestions: (p: string) => Promise<SelectValue[]>;
|
||||||
valueSelected: (p: SelectValue) => void;
|
valueSelected: (p: SelectValue) => void;
|
||||||
label: string;
|
label?: string;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
value?: SelectValue;
|
value?: SelectValue;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
loadingMessage: string;
|
loadingMessage: string;
|
||||||
noOptionsMessage: string;
|
noOptionsMessage: string;
|
||||||
creatable?: boolean;
|
creatable?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {};
|
type State = {};
|
||||||
@@ -53,10 +55,11 @@ class Autocomplete extends React.Component<Props, State> {
|
|||||||
loadingMessage,
|
loadingMessage,
|
||||||
noOptionsMessage,
|
noOptionsMessage,
|
||||||
loadSuggestions,
|
loadSuggestions,
|
||||||
creatable
|
creatable,
|
||||||
|
className
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className={classNames("field", className)}>
|
||||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||||
<div className="control">
|
<div className="control">
|
||||||
{creatable ? (
|
{creatable ? (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types";
|
import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types";
|
||||||
import Autocomplete from "./Autocomplete";
|
import Autocomplete from "./Autocomplete";
|
||||||
|
import { apiClient } from "./apiclient";
|
||||||
|
|
||||||
export type AutocompleteProps = {
|
export type AutocompleteProps = {
|
||||||
autocompleteLink?: string;
|
autocompleteLink?: string;
|
||||||
@@ -19,7 +20,8 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
|
|||||||
loadSuggestions = (inputValue: string): Promise<SelectValue[]> => {
|
loadSuggestions = (inputValue: string): Promise<SelectValue[]> => {
|
||||||
const url = this.props.autocompleteLink;
|
const url = this.props.autocompleteLink;
|
||||||
const link = url + "?q=";
|
const link = url + "?q=";
|
||||||
return fetch(link + inputValue)
|
return apiClient
|
||||||
|
.get(link + inputValue)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((json: AutocompleteObject[]) => {
|
.then((json: AutocompleteObject[]) => {
|
||||||
return json.map(element => {
|
return json.map(element => {
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-gipzik xsalO"
|
className="sc-fBuWsC ldmpJA"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="field"
|
className="field"
|
||||||
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-gipzik xsalO"
|
className="sc-fBuWsC ldmpJA"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="field"
|
className="field"
|
||||||
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Radio Default 1`] = `
|
exports[`Storyshots Forms|Radio Default 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-csuQGl fFFkRK"
|
className="sc-fMiknA keSQNk"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="radio"
|
className="radio"
|
||||||
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
|
|||||||
|
|
||||||
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
||||||
<div
|
<div
|
||||||
className="sc-csuQGl fFFkRK"
|
className="sc-fMiknA keSQNk"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="radio"
|
className="radio"
|
||||||
@@ -456,6 +456,83 @@ exports[`Storyshots Forms|Radio Disabled 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Forms|Textarea OnCancel 1`] = `
|
||||||
|
<div
|
||||||
|
className="sc-dVhcbM ituOZx"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
value="Use the escape key to clear the textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Forms|Textarea OnChange 1`] = `
|
||||||
|
<div
|
||||||
|
className="sc-dVhcbM ituOZx"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
value="Start typing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
Start typing
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Forms|Textarea OnSubmit 1`] = `
|
||||||
|
<div
|
||||||
|
className="sc-dVhcbM ituOZx"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="field"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
className="textarea"
|
||||||
|
disabled={false}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
value="Use the ctrl/command + Enter to submit the textarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Loading Default 1`] = `
|
exports[`Storyshots Loading Default 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -2311,3 +2388,173 @@ PORT_NUMBER =
|
|||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Table|Table Default 1`] = `
|
||||||
|
<table
|
||||||
|
className="sc-jhAzac hmXDXQ table content is-hoverable"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
First Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="has-cursor-pointer"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
Last Name
|
||||||
|
<i
|
||||||
|
className="fas fa-sort-amount-down has-text-grey-light sc-hzDkRC escBde"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
E-Mail
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h4>
|
||||||
|
Tricia
|
||||||
|
</h4>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
McMillan
|
||||||
|
</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a>
|
||||||
|
tricia@hitchhiker.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h4>
|
||||||
|
Arthur
|
||||||
|
</h4>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<b
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "red",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Dent
|
||||||
|
</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a>
|
||||||
|
arthur@hitchhiker.com
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Table|Table Empty 1`] = `
|
||||||
|
<div
|
||||||
|
className="notification is-info"
|
||||||
|
>
|
||||||
|
|
||||||
|
No data found.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Table|Table TextColumn 1`] = `
|
||||||
|
<table
|
||||||
|
className="sc-jhAzac hmXDXQ table content is-hoverable"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
className="has-cursor-pointer"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
Id
|
||||||
|
<i
|
||||||
|
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="has-cursor-pointer"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
<i
|
||||||
|
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="has-cursor-pointer"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
<i
|
||||||
|
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
21
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Pommes
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Fried potato sticks
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
42
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Quarter-Pounder
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Big burger
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
-84
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Icecream
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Cold dessert
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|||||||
@@ -27,13 +27,17 @@ const extractXsrfToken = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
||||||
const headers: { [key: string]: string } = {
|
if (!o.headers) {
|
||||||
Cache: "no-cache",
|
o.headers = {};
|
||||||
// identify the request as ajax request
|
}
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
// identify the web interface
|
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
|
||||||
"X-SCM-Client": "WUI"
|
const headers: Record<string, string> = o.headers;
|
||||||
};
|
headers["Cache"] = "no-cache";
|
||||||
|
// identify the request as ajax request
|
||||||
|
headers["X-Requested-With"] = "XMLHttpRequest";
|
||||||
|
// identify the web interface
|
||||||
|
headers["X-SCM-Client"] = "WUI";
|
||||||
|
|
||||||
const xsrf = extractXsrfToken();
|
const xsrf = extractXsrfToken();
|
||||||
if (xsrf) {
|
if (xsrf) {
|
||||||
@@ -80,11 +84,19 @@ class ApiClient {
|
|||||||
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
|
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
post(url: string, payload?: any, contentType = "application/json", additionalHeaders = new Headers()) {
|
post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
|
||||||
return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
|
return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders = new Headers()) {
|
postText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
|
||||||
|
return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
putText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
|
||||||
|
return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
fileAppender(formData);
|
fileAppender(formData);
|
||||||
|
|
||||||
@@ -96,7 +108,7 @@ class ApiClient {
|
|||||||
return this.httpRequestWithBinaryBody(options, url);
|
return this.httpRequestWithBinaryBody(options, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
put(url: string, payload: any, contentType = "application/json", additionalHeaders = new Headers()) {
|
put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
|
||||||
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
|
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +132,7 @@ class ApiClient {
|
|||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
contentType: string,
|
contentType: string,
|
||||||
additionalHeaders: Headers,
|
additionalHeaders: Record<string, string>,
|
||||||
payload?: any
|
payload?: any
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const options: RequestInit = {
|
const options: RequestInit = {
|
||||||
@@ -133,13 +145,27 @@ class ApiClient {
|
|||||||
return this.httpRequestWithBinaryBody(options, url, contentType);
|
return this.httpRequestWithBinaryBody(options, url, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpRequestWithTextBody(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
additionalHeaders: Record<string, string> = {},
|
||||||
|
payload: string
|
||||||
|
) {
|
||||||
|
const options: RequestInit = {
|
||||||
|
method: method,
|
||||||
|
headers: additionalHeaders
|
||||||
|
};
|
||||||
|
options.body = payload;
|
||||||
|
return this.httpRequestWithBinaryBody(options, url, "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) {
|
httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) {
|
||||||
options = applyFetchOptions(options);
|
options = applyFetchOptions(options);
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
if (!options.headers) {
|
if (!options.headers) {
|
||||||
options.headers = new Headers();
|
options.headers = {};
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
|
||||||
options.headers["Content-Type"] = contentType;
|
options.headers["Content-Type"] = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from "react";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
url: string;
|
url?: string;
|
||||||
disabled: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
126
scm-ui/ui-components/src/comparators.test.ts
Normal file
126
scm-ui/ui-components/src/comparators.test.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { byKey, byValueLength, byNestedKeys } from "./comparators";
|
||||||
|
|
||||||
|
const createObject = (key: string, value?: string) => {
|
||||||
|
return {
|
||||||
|
[key]: value
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createObjects = (key: string, values: Array<string | undefined>) => {
|
||||||
|
return values.map(v => createObject(key, v));
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("key comparator tests", () => {
|
||||||
|
it("should sort array", () => {
|
||||||
|
const array = createObjects("key", ["z", "a", "y", "b"]);
|
||||||
|
array.sort(byKey("key"));
|
||||||
|
expect(array).toEqual(createObjects("key", ["a", "b", "y", "z"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if value is undefined", () => {
|
||||||
|
const array = createObjects("key", ["z", undefined, "a"]);
|
||||||
|
array.sort(byKey("key"));
|
||||||
|
expect(array).toEqual(createObjects("key", ["a", "z", undefined]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if key is undefined", () => {
|
||||||
|
const array = createObjects("key", ["a"]);
|
||||||
|
array.push({});
|
||||||
|
array.push(createObject("key", "z"));
|
||||||
|
array.sort(byKey("key"));
|
||||||
|
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), {}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if item is undefined", () => {
|
||||||
|
const array: any[] = createObjects("key", ["a"]);
|
||||||
|
array.push(undefined);
|
||||||
|
array.push(createObject("key", "z"));
|
||||||
|
array.sort(byKey("key"));
|
||||||
|
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), undefined]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("length comparator tests", () => {
|
||||||
|
it("should sort array", () => {
|
||||||
|
const array = createObjects("key", ["....", ".", "...", ".."]);
|
||||||
|
array.sort(byValueLength("key"));
|
||||||
|
expect(array).toEqual(createObjects("key", [".", "..", "...", "...."]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if value is undefined", () => {
|
||||||
|
const array = createObjects("key", ["..", undefined, "."]);
|
||||||
|
array.sort(byValueLength("key"));
|
||||||
|
expect(array).toEqual(createObjects("key", [".", "..", undefined]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if key is undefined", () => {
|
||||||
|
const array = createObjects("key", ["."]);
|
||||||
|
array.push({});
|
||||||
|
array.push(createObject("key", ".."));
|
||||||
|
array.sort(byValueLength("key"));
|
||||||
|
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), {}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if item is undefined", () => {
|
||||||
|
const array: any[] = createObjects("key", ["."]);
|
||||||
|
array.push(undefined);
|
||||||
|
array.push(createObject("key", ".."));
|
||||||
|
array.sort(byValueLength("key"));
|
||||||
|
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), undefined]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("nested key comparator tests", () => {
|
||||||
|
const createObject = (key: string, nested?: string, value?: string) => {
|
||||||
|
if (!nested) {
|
||||||
|
return {
|
||||||
|
[key]: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[key]: {
|
||||||
|
[nested]: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createObjects = (key: string, nested: string, values: Array<string | undefined>) => {
|
||||||
|
return values.map(v => createObject(key, nested, v));
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should sort array", () => {
|
||||||
|
const array = createObjects("key", "nested", ["z", "a", "y", "b"]);
|
||||||
|
array.sort(byNestedKeys("key", "nested"));
|
||||||
|
expect(array).toEqual(createObjects("key", "nested", ["a", "b", "y", "z"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if value is undefined", () => {
|
||||||
|
const array = createObjects("key", "nested", ["z", undefined, "a"]);
|
||||||
|
array.sort(byNestedKeys("key", "nested"));
|
||||||
|
expect(array).toEqual(createObjects("key", "nested", ["a", "z", undefined]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if key is undefined", () => {
|
||||||
|
const array = createObjects("key", "nested", ["a"]);
|
||||||
|
array.push({});
|
||||||
|
array.push(createObject("key", "nested", "z"));
|
||||||
|
array.sort(byNestedKeys("key", "nested"));
|
||||||
|
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), {}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if nested key is undefined", () => {
|
||||||
|
const array = createObjects("key", "nested", ["a"]);
|
||||||
|
array.push(createObject("key", undefined, "y"));
|
||||||
|
array.push(createObject("key", "nested", "z"));
|
||||||
|
array.sort(byNestedKeys("key", "nested"));
|
||||||
|
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), { key: undefined }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fail if item is undefined", () => {
|
||||||
|
const array: any[] = createObjects("key", "nested", ["a"]);
|
||||||
|
array.push(undefined);
|
||||||
|
array.push(createObject("key", "nested", "z"));
|
||||||
|
array.sort(byNestedKeys("key", "nested"));
|
||||||
|
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), undefined]);
|
||||||
|
});
|
||||||
|
});
|
||||||
75
scm-ui/ui-components/src/comparators.ts
Normal file
75
scm-ui/ui-components/src/comparators.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const isUndefined = (o: any, key: string, nested?: string) => {
|
||||||
|
if (typeof o === "undefined" || typeof o[key] === "undefined") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (nested) {
|
||||||
|
return typeof o[key][nested] === "undefined";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const byKey = (key: string) => {
|
||||||
|
return (a: any, b: any) => {
|
||||||
|
if (isUndefined(a, key)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(b, key)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a[key] < b[key]) {
|
||||||
|
return -1;
|
||||||
|
} else if (a[key] > b[key]) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const byValueLength = (key: string) => {
|
||||||
|
return (a: any, b: any) => {
|
||||||
|
if (isUndefined(a, key)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(b, key)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a[key].length < b[key].length) {
|
||||||
|
return -1;
|
||||||
|
} else if (a[key].length > b[key].length) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const byNestedKeys = (key: string, nestedKey: string) => {
|
||||||
|
return (a: any, b: any) => {
|
||||||
|
if (isUndefined(a, key, nestedKey)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(b, key, nestedKey)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a[key][nestedKey] < b[key][nestedKey]) {
|
||||||
|
return -1;
|
||||||
|
} else if (a[key][nestedKey] > b[key][nestedKey]) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
byKey,
|
||||||
|
byValueLength,
|
||||||
|
byNestedKeys
|
||||||
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react";
|
import React, { FormEvent } from "react";
|
||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
import { Links, Link } from "@scm-manager/ui-types";
|
import { Links, Link } from "@scm-manager/ui-types";
|
||||||
import { apiClient, SubmitButton, Loading, ErrorNotification } from "../";
|
import { apiClient, Level, SubmitButton, Loading, ErrorNotification } from "../";
|
||||||
import { FormEvent } from "react";
|
|
||||||
|
|
||||||
type RenderProps = {
|
type RenderProps = {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
@@ -179,7 +178,9 @@ class Configuration extends React.Component<Props, State> {
|
|||||||
<form onSubmit={this.modifyConfiguration}>
|
<form onSubmit={this.modifyConfiguration}>
|
||||||
{this.props.render(renderProps)}
|
{this.props.render(renderProps)}
|
||||||
<hr />
|
<hr />
|
||||||
<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} />
|
<Level
|
||||||
|
right={<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} />}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { MouseEvent } from "react";
|
import React, { MouseEvent } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
import { AddButton } from "../buttons";
|
import Level from "../layout/Level";
|
||||||
import InputField from "./InputField";
|
import InputField from "./InputField";
|
||||||
|
import AddButton from "../buttons/AddButton";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
addEntry: (p: string) => void;
|
addEntry: (p: string) => void;
|
||||||
@@ -17,6 +18,22 @@ type State = {
|
|||||||
entryToAdd: string;
|
entryToAdd: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledLevel = styled(Level)`
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 1rem !important; // same margin as field
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInputField = styled(InputField)`
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledField = styled.div.attrs(props => ({
|
||||||
|
className: "field"
|
||||||
|
}))`
|
||||||
|
align-self: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
class AddEntryToTableField extends React.Component<Props, State> {
|
class AddEntryToTableField extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -37,23 +54,29 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props;
|
const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledLevel
|
||||||
<InputField
|
children={
|
||||||
label={fieldLabel}
|
<StyledInputField
|
||||||
errorMessage={errorMessage}
|
label={fieldLabel}
|
||||||
onChange={this.handleAddEntryChange}
|
errorMessage={errorMessage}
|
||||||
validationError={!this.isValid()}
|
onChange={this.handleAddEntryChange}
|
||||||
value={this.state.entryToAdd}
|
validationError={!this.isValid()}
|
||||||
onReturnPressed={this.appendEntry}
|
value={this.state.entryToAdd}
|
||||||
disabled={disabled}
|
onReturnPressed={this.appendEntry}
|
||||||
helpText={helpText}
|
disabled={disabled}
|
||||||
/>
|
helpText={helpText}
|
||||||
<AddButton
|
/>
|
||||||
label={buttonLabel}
|
}
|
||||||
action={this.addButtonClicked}
|
right={
|
||||||
disabled={disabled || this.state.entryToAdd === "" || !this.isValid()}
|
<StyledField>
|
||||||
/>
|
<AddButton
|
||||||
</>
|
label={buttonLabel}
|
||||||
|
action={this.addButtonClicked}
|
||||||
|
disabled={disabled || this.state.entryToAdd === "" || !this.isValid()}
|
||||||
|
/>
|
||||||
|
</StyledField>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { MouseEvent } from "react";
|
import React, { MouseEvent } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
import { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
|
import { SelectValue } from "@scm-manager/ui-types";
|
||||||
|
import Level from "../layout/Level";
|
||||||
import Autocomplete from "../Autocomplete";
|
import Autocomplete from "../Autocomplete";
|
||||||
import AddButton from "../buttons/AddButton";
|
import AddButton from "../buttons/AddButton";
|
||||||
|
|
||||||
@@ -20,6 +21,11 @@ type State = {
|
|||||||
selectedValue?: SelectValue;
|
selectedValue?: SelectValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledAutocomplete = styled(Autocomplete)`
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -41,21 +47,26 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
|||||||
|
|
||||||
const { selectedValue } = this.state;
|
const { selectedValue } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<Level
|
||||||
<Autocomplete
|
children={
|
||||||
label={fieldLabel}
|
<StyledAutocomplete
|
||||||
loadSuggestions={loadSuggestions}
|
label={fieldLabel}
|
||||||
valueSelected={this.handleAddEntryChange}
|
loadSuggestions={loadSuggestions}
|
||||||
helpText={helpText}
|
valueSelected={this.handleAddEntryChange}
|
||||||
value={selectedValue}
|
helpText={helpText}
|
||||||
placeholder={placeholder}
|
value={selectedValue}
|
||||||
loadingMessage={loadingMessage}
|
placeholder={placeholder}
|
||||||
noOptionsMessage={noOptionsMessage}
|
loadingMessage={loadingMessage}
|
||||||
creatable={true}
|
noOptionsMessage={noOptionsMessage}
|
||||||
/>
|
creatable={true}
|
||||||
|
/>
|
||||||
<AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} />
|
}
|
||||||
</div>
|
right={
|
||||||
|
<div className="field">
|
||||||
|
<AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Props = {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputField extends React.Component<Props> {
|
class InputField extends React.Component<Props> {
|
||||||
@@ -47,11 +48,21 @@ class InputField extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { type, placeholder, value, validationError, errorMessage, disabled, label, helpText } = this.props;
|
const {
|
||||||
|
type,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
validationError,
|
||||||
|
errorMessage,
|
||||||
|
disabled,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
className
|
||||||
|
} = this.props;
|
||||||
const errorView = validationError ? "is-danger" : "";
|
const errorView = validationError ? "is-danger" : "";
|
||||||
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className={classNames("field", className)}>
|
||||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<input
|
<input
|
||||||
|
|||||||
56
scm-ui/ui-components/src/forms/Textarea.stories.tsx
Normal file
56
scm-ui/ui-components/src/forms/Textarea.stories.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, {useState} from "react";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Textarea from "./Textarea";
|
||||||
|
|
||||||
|
const Spacing = styled.div`
|
||||||
|
padding: 2em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OnChangeTextarea = () => {
|
||||||
|
const [value, setValue] = useState("Start typing");
|
||||||
|
return (
|
||||||
|
<Spacing>
|
||||||
|
<Textarea value={value} onChange={v => setValue(v)} />
|
||||||
|
<hr />
|
||||||
|
<p>{value}</p>
|
||||||
|
</Spacing>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnSubmitTextare = () => {
|
||||||
|
const [value, setValue] = useState("Use the ctrl/command + Enter to submit the textarea");
|
||||||
|
const [submitted, setSubmitted] = useState("");
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
setSubmitted(value);
|
||||||
|
setValue("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spacing>
|
||||||
|
<Textarea value={value} onChange={v => setValue(v)} onSubmit={submit} />
|
||||||
|
<hr />
|
||||||
|
<p>{submitted}</p>
|
||||||
|
</Spacing>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OnCancelTextare = () => {
|
||||||
|
const [value, setValue] = useState("Use the escape key to clear the textarea");
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
setValue("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spacing>
|
||||||
|
<Textarea value={value} onChange={v => setValue(v)} onCancel={cancel} />
|
||||||
|
</Spacing>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
storiesOf("Forms|Textarea", module)
|
||||||
|
.add("OnChange", () => <OnChangeTextarea />)
|
||||||
|
.add("OnSubmit", () => <OnSubmitTextare />)
|
||||||
|
.add("OnCancel", () => <OnCancelTextare />);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ChangeEvent } from "react";
|
import React, { ChangeEvent, KeyboardEvent } from "react";
|
||||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -10,6 +10,8 @@ type Props = {
|
|||||||
onChange: (value: string, name?: string) => void;
|
onChange: (value: string, name?: string) => void;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onSubmit?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Textarea extends React.Component<Props> {
|
class Textarea extends React.Component<Props> {
|
||||||
@@ -25,6 +27,19 @@ class Textarea extends React.Component<Props> {
|
|||||||
this.props.onChange(event.target.value, this.props.name);
|
this.props.onChange(event.target.value, this.props.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
const { onCancel } = this.props;
|
||||||
|
if (onCancel && event.key === "Escape") {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onSubmit } = this.props;
|
||||||
|
if (onSubmit && event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { placeholder, value, label, helpText, disabled } = this.props;
|
const { placeholder, value, label, helpText, disabled } = this.props;
|
||||||
|
|
||||||
@@ -41,6 +56,7 @@ class Textarea extends React.Component<Props> {
|
|||||||
onChange={this.handleInput}
|
onChange={this.handleInput}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export { default as OverviewPageActions } from "./OverviewPageActions";
|
|||||||
export { default as CardColumnGroup } from "./CardColumnGroup";
|
export { default as CardColumnGroup } from "./CardColumnGroup";
|
||||||
export { default as CardColumn } from "./CardColumn";
|
export { default as CardColumn } from "./CardColumn";
|
||||||
|
|
||||||
|
export { default as comparators } from "./comparators";
|
||||||
|
|
||||||
export { apiClient } from "./apiclient";
|
export { apiClient } from "./apiclient";
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ export * from "./layout";
|
|||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
export * from "./navigation";
|
export * from "./navigation";
|
||||||
export * from "./repos";
|
export * from "./repos";
|
||||||
|
export * from "./table";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
File,
|
File,
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ import classNames from "classnames";
|
|||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
left?: ReactNode;
|
left?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Level extends React.Component<Props> {
|
export default class Level extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { className, left, right } = this.props;
|
const { className, left, children, right } = this.props;
|
||||||
|
let child = null;
|
||||||
|
if (children) {
|
||||||
|
child = <div className="level-item">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("level", className)}>
|
<div className={classNames("level", className)}>
|
||||||
<div className="level-left">{left}</div>
|
<div className="level-left">{left}</div>
|
||||||
|
{child}
|
||||||
<div className="level-right">{right}</div>
|
<div className="level-right">{right}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import DiffFile from "./DiffFile";
|
import DiffFile from "./DiffFile";
|
||||||
import { DiffObjectProps, File } from "./DiffTypes";
|
import { DiffObjectProps, File } from "./DiffTypes";
|
||||||
|
import Notification from "../Notification";
|
||||||
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props = DiffObjectProps & {
|
type Props = WithTranslation &
|
||||||
diff: File[];
|
DiffObjectProps & {
|
||||||
defaultCollapse?: boolean;
|
diff: File[];
|
||||||
};
|
defaultCollapse?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
class Diff extends React.Component<Props> {
|
class Diff extends React.Component<Props> {
|
||||||
static defaultProps: Partial<Props> = {
|
static defaultProps: Partial<Props> = {
|
||||||
@@ -13,15 +16,17 @@ class Diff extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { diff, ...fileProps } = this.props;
|
const { diff, t, ...fileProps } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{diff.map((file, index) => (
|
{diff.length === 0 ? (
|
||||||
<DiffFile key={index} file={file} {...fileProps} {...this.props} />
|
<Notification type="info">{t("diff.noDiffFound")}</Notification>
|
||||||
))}
|
) : (
|
||||||
|
diff.map((file, index) => <DiffFile key={index} file={file} {...fileProps} {...this.props} />)
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Diff;
|
export default withTranslation("repos")(Diff);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class LoadingDiff extends React.Component<Props, State> {
|
|||||||
|
|
||||||
fetchDiff = () => {
|
fetchDiff = () => {
|
||||||
const { url } = this.props;
|
const { url } = this.props;
|
||||||
|
this.setState({loading: true});
|
||||||
apiClient
|
apiClient
|
||||||
.get(url)
|
.get(url)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
|
|||||||
18
scm-ui/ui-components/src/table/Column.tsx
Normal file
18
scm-ui/ui-components/src/table/Column.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { ColumnProps } from "./types";
|
||||||
|
|
||||||
|
type Props = ColumnProps & {
|
||||||
|
children: (row: any, columnIndex: number) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Column: FC<Props> = ({ row, columnIndex, children }) => {
|
||||||
|
if (row === undefined) {
|
||||||
|
throw new Error("missing row, use column only as child of Table");
|
||||||
|
}
|
||||||
|
if (columnIndex === undefined) {
|
||||||
|
throw new Error("missing row, use column only as child of Table");
|
||||||
|
}
|
||||||
|
return <>{children(row, columnIndex)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Column;
|
||||||
19
scm-ui/ui-components/src/table/SortIcon.tsx
Normal file
19
scm-ui/ui-components/src/table/SortIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Icon from "../Icon";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconWithMarginLeft = styled(Icon)`
|
||||||
|
visibility: ${(props: Props) => (props.isVisible ? "visible" : "hidden")};
|
||||||
|
margin-left: 0.25em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SortIcon: FC<Props> = (props: Props) => {
|
||||||
|
return <IconWithMarginLeft name={props.name} isVisible={props.isVisible} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortIcon;
|
||||||
53
scm-ui/ui-components/src/table/Table.stories.tsx
Normal file
53
scm-ui/ui-components/src/table/Table.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { storiesOf } from "@storybook/react";
|
||||||
|
import Table from "./Table";
|
||||||
|
import Column from "./Column";
|
||||||
|
import TextColumn from "./TextColumn";
|
||||||
|
|
||||||
|
storiesOf("Table|Table", module)
|
||||||
|
.add("Default", () => (
|
||||||
|
<Table
|
||||||
|
data={[
|
||||||
|
{ firstname: "Tricia", lastname: "McMillan", email: "tricia@hitchhiker.com" },
|
||||||
|
{ firstname: "Arthur", lastname: "Dent", email: "arthur@hitchhiker.com" }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Column header={"First Name"}>{(row: any) => <h4>{row.firstname}</h4>}</Column>
|
||||||
|
<Column
|
||||||
|
header={"Last Name"}
|
||||||
|
createComparator={() => {
|
||||||
|
return (a: any, b: any) => {
|
||||||
|
if (a.lastname > b.lastname) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.lastname < b.lastname) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(row: any) => <b style={{ color: "red" }}>{row.lastname}</b>}
|
||||||
|
</Column>
|
||||||
|
<Column header={"E-Mail"}>{(row: any) => <a>{row.email}</a>}</Column>
|
||||||
|
</Table>
|
||||||
|
))
|
||||||
|
.add("TextColumn", () => (
|
||||||
|
<Table
|
||||||
|
data={[
|
||||||
|
{ id: "21", title: "Pommes", desc: "Fried potato sticks" },
|
||||||
|
{ id: "42", title: "Quarter-Pounder", desc: "Big burger" },
|
||||||
|
{ id: "-84", title: "Icecream", desc: "Cold dessert" }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TextColumn header="Id" dataKey="id" />
|
||||||
|
<TextColumn header="Name" dataKey="title" />
|
||||||
|
<TextColumn header="Description" dataKey="desc" />
|
||||||
|
</Table>
|
||||||
|
))
|
||||||
|
.add("Empty", () => (
|
||||||
|
<Table data={[]} emptyMessage="No data found.">
|
||||||
|
<TextColumn header="Id" dataKey="id" />
|
||||||
|
<TextColumn header="Name" dataKey="name" />
|
||||||
|
</Table>
|
||||||
|
));
|
||||||
120
scm-ui/ui-components/src/table/Table.tsx
Normal file
120
scm-ui/ui-components/src/table/Table.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { FC, ReactElement, useEffect, useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { Comparator } from "./types";
|
||||||
|
import SortIcon from "./SortIcon";
|
||||||
|
import Notification from "../Notification";
|
||||||
|
|
||||||
|
const StyledTable = styled.table.attrs(() => ({
|
||||||
|
className: "table content is-hoverable"
|
||||||
|
}))``;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: any[];
|
||||||
|
sortable?: boolean;
|
||||||
|
emptyMessage?: string;
|
||||||
|
children: Array<ReactElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
|
||||||
|
const [tableData, setTableData] = useState(data);
|
||||||
|
useEffect(() => {
|
||||||
|
setTableData(data);
|
||||||
|
}, [data]);
|
||||||
|
const [ascending, setAscending] = useState(false);
|
||||||
|
const [lastSortBy, setlastSortBy] = useState<number | undefined>();
|
||||||
|
const [hoveredColumnIndex, setHoveredColumnIndex] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const isSortable = (child: ReactElement) => {
|
||||||
|
return sortable && child.props.createComparator;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortFunctions: Comparator | undefined[] = [];
|
||||||
|
React.Children.forEach(children, (child, index) => {
|
||||||
|
if (child && isSortable(child)) {
|
||||||
|
sortFunctions.push(child.props.createComparator(child.props, index));
|
||||||
|
} else {
|
||||||
|
sortFunctions.push(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDataToColumns = (row: any) => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
{React.Children.map(children, (child, columnIndex) => {
|
||||||
|
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortDescending = (sortAscending: (a: any, b: any) => number) => {
|
||||||
|
return (a: any, b: any) => {
|
||||||
|
return sortAscending(a, b) * -1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableSort = (index: number) => {
|
||||||
|
const sortFn = sortFunctions[index];
|
||||||
|
if (!sortFn) {
|
||||||
|
throw new Error(`column with index ${index} is not sortable`);
|
||||||
|
}
|
||||||
|
const sortableData = [...tableData];
|
||||||
|
let sortOrder = ascending;
|
||||||
|
if (lastSortBy !== index) {
|
||||||
|
setAscending(true);
|
||||||
|
sortOrder = true;
|
||||||
|
}
|
||||||
|
const sortFunction = sortOrder ? sortFn : sortDescending(sortFn);
|
||||||
|
sortableData.sort(sortFunction);
|
||||||
|
setTableData(sortableData);
|
||||||
|
setAscending(!sortOrder);
|
||||||
|
setlastSortBy(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldShowIcon = (index: number) => {
|
||||||
|
return index === lastSortBy || index === hoveredColumnIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tableData || tableData.length <= 0) {
|
||||||
|
if (emptyMessage) {
|
||||||
|
return <Notification type="info">{emptyMessage}</Notification>;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledTable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{React.Children.map(children, (child, index) => (
|
||||||
|
<th
|
||||||
|
className={isSortable(child) && "has-cursor-pointer"}
|
||||||
|
onClick={isSortable(child) ? () => tableSort(index) : undefined}
|
||||||
|
onMouseEnter={() => setHoveredColumnIndex(index)}
|
||||||
|
onMouseLeave={() => setHoveredColumnIndex(undefined)}
|
||||||
|
>
|
||||||
|
{child.props.header}
|
||||||
|
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{tableData.map(mapDataToColumns)}</tbody>
|
||||||
|
</StyledTable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Table.defaultProps = {
|
||||||
|
sortable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSortIcon = (child: ReactElement, ascending: boolean, showIcon: boolean) => {
|
||||||
|
if (child.props.ascendingIcon && child.props.descendingIcon) {
|
||||||
|
return <SortIcon name={ascending ? child.props.ascendingIcon : child.props.descendingIcon} isVisible={showIcon} />;
|
||||||
|
} else {
|
||||||
|
return <SortIcon name={ascending ? "sort-amount-down-alt" : "sort-amount-down"} isVisible={showIcon} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Table;
|
||||||
21
scm-ui/ui-components/src/table/TextColumn.tsx
Normal file
21
scm-ui/ui-components/src/table/TextColumn.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { ColumnProps } from "./types";
|
||||||
|
import comparators from "../comparators";
|
||||||
|
|
||||||
|
type Props = ColumnProps & {
|
||||||
|
dataKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextColumn: FC<Props> = ({ row, dataKey }) => {
|
||||||
|
return row[dataKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
TextColumn.defaultProps = {
|
||||||
|
createComparator: (props: Props) => {
|
||||||
|
return comparators.byKey(props.dataKey);
|
||||||
|
},
|
||||||
|
ascendingIcon: "sort-alpha-down-alt",
|
||||||
|
descendingIcon: "sort-alpha-down"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextColumn;
|
||||||
4
scm-ui/ui-components/src/table/index.ts
Normal file
4
scm-ui/ui-components/src/table/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as Table } from "./Table";
|
||||||
|
export { default as Column } from "./Column";
|
||||||
|
export { default as TextColumn } from "./TextColumn";
|
||||||
|
export { default as SortIcon } from "./SortIcon";
|
||||||
12
scm-ui/ui-components/src/table/types.ts
Normal file
12
scm-ui/ui-components/src/table/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export type Comparator = (a: any, b: any) => number;
|
||||||
|
|
||||||
|
export type ColumnProps = {
|
||||||
|
header: ReactNode;
|
||||||
|
row?: any;
|
||||||
|
columnIndex?: number;
|
||||||
|
createComparator?: (props: any, columnIndex: number) => Comparator;
|
||||||
|
ascendingIcon?: string;
|
||||||
|
descendingIcon?: string;
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"@types/enzyme": "^3.10.3",
|
"@types/enzyme": "^3.10.3",
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^24.0.19",
|
||||||
"@types/react": "^16.9.9",
|
"@types/react": "^16.9.9",
|
||||||
"typescript": "^3.6.4"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
const { spawnSync } = require("child_process");
|
const { spawnSync } = require("child_process");
|
||||||
|
|
||||||
const commands = ["plugin", "plugin-watch", "publish"];
|
const commands = ["plugin", "plugin-watch", "publish", "version"];
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const versions = require("../versions");
|
|||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
console.log("usage ui-scripts publish version");
|
console.log("usage ui-scripts publish <version>");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
scm-ui/ui-scripts/src/commands/version.js
Normal file
12
scm-ui/ui-scripts/src/commands/version.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const lerna = require("../lerna");
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length < 1) {
|
||||||
|
console.log("usage ui-scripts version <new-version>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = args[0];
|
||||||
|
|
||||||
|
lerna.version(version);
|
||||||
@@ -7,41 +7,6 @@ const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
|||||||
const root = path.resolve(process.cwd(), "scm-ui");
|
const root = path.resolve(process.cwd(), "scm-ui");
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
|
||||||
context: root,
|
|
||||||
entry: "./ui-styles/src/scm.scss",
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(css|scss|sass)$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: MiniCssExtractPlugin.loader
|
|
||||||
},
|
|
||||||
"css-loader",
|
|
||||||
"sass-loader"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
|
|
||||||
use: ["file-loader"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: "ui-styles.css",
|
|
||||||
ignoreOrder: false
|
|
||||||
})
|
|
||||||
],
|
|
||||||
optimization: {
|
|
||||||
minimizer: [new OptimizeCSSAssetsPlugin({})]
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.join(root, "target", "assets"),
|
|
||||||
filename: "ui-styles.bundle.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
context: root,
|
context: root,
|
||||||
entry: {
|
entry: {
|
||||||
@@ -142,6 +107,41 @@ module.exports = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
context: root,
|
||||||
|
entry: "./ui-styles/src/scm.scss",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(css|scss|sass)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: MiniCssExtractPlugin.loader
|
||||||
|
},
|
||||||
|
"css-loader",
|
||||||
|
"sass-loader"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
|
||||||
|
use: ["file-loader"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "ui-styles.css",
|
||||||
|
ignoreOrder: false
|
||||||
|
})
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimizer: [new OptimizeCSSAssetsPlugin({})]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.join(root, "target", "assets"),
|
||||||
|
filename: "ui-styles.bundle.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
context: path.resolve(root),
|
context: path.resolve(root),
|
||||||
entry: {
|
entry: {
|
||||||
|
|||||||
@@ -622,6 +622,10 @@ form .field:not(.is-grouped) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
// label with help-icon compensation
|
// label with help-icon compensation
|
||||||
.label-icon-spacing {
|
.label-icon-spacing {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
@@ -809,10 +813,6 @@ form .field:not(.is-grouped) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-card-body div div:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cursor
|
// cursor
|
||||||
.has-cursor-pointer {
|
.has-cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -26,6 +26,6 @@
|
|||||||
"@types/enzyme": "^3.10.3",
|
"@types/enzyme": "^3.10.3",
|
||||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^24.0.19",
|
||||||
"typescript": "^3.6.4"
|
"typescript": "^3.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^3.6.4"
|
"typescript": "^3.7.2"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
"systemjs": "0.21.6"
|
"systemjs": "0.21.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest"
|
||||||
"flow": "flow"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",
|
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"settingsNavLink": "Einstellungen",
|
"settingsNavLink": "Einstellungen",
|
||||||
"generalNavLink": "Generell"
|
"generalNavLink": "Generell"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"currentAppVersion": "Aktuelle Software-Versionsnummer",
|
"currentAppVersion": "Aktuelle Software-Versionsnummer",
|
||||||
"communityTitle": "Community Support",
|
"communityTitle": "Community Support",
|
||||||
"communityIconAlt": "Community Support Icon",
|
"communityIconAlt": "Community Support Icon",
|
||||||
@@ -91,8 +91,7 @@
|
|||||||
"submit": "Speichern"
|
"submit": "Speichern"
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"button": "Löschen",
|
"button": "Berechtigungsrolle löschen",
|
||||||
"subtitle": "Berechtigungsrolle löschen",
|
|
||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Berechtigungsrolle löschen?",
|
"title": "Berechtigungsrolle löschen?",
|
||||||
"message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.",
|
"message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.",
|
||||||
|
|||||||
@@ -60,8 +60,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deleteGroup": {
|
"deleteGroup": {
|
||||||
"subtitle": "Gruppe löschen",
|
"button": "Gruppe löschen",
|
||||||
"button": "Löschen",
|
|
||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Gruppe löschen",
|
"title": "Gruppe löschen",
|
||||||
"message": "Soll die Gruppe wirklich gelöscht werden?",
|
"message": "Soll die Gruppe wirklich gelöscht werden?",
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
"size": "Größe"
|
"size": "Größe"
|
||||||
},
|
},
|
||||||
"noSources": "Keine Sources in diesem Branch gefunden.",
|
"noSources": "Keine Sources in diesem Branch gefunden.",
|
||||||
"extension" : {
|
"extension": {
|
||||||
"notBound": "Keine Erweiterung angebunden."
|
"notBound": "Keine Erweiterung angebunden."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -166,8 +166,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deleteRepo": {
|
"deleteRepo": {
|
||||||
"subtitle": "Repository löschen",
|
"button": "Repository löschen",
|
||||||
"button": "Löschen",
|
|
||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Repository löschen",
|
"title": "Repository löschen",
|
||||||
"message": "Soll das Repository wirklich gelöscht werden?",
|
"message": "Soll das Repository wirklich gelöscht werden?",
|
||||||
@@ -177,7 +176,8 @@
|
|||||||
},
|
},
|
||||||
"diff": {
|
"diff": {
|
||||||
"sideBySide": "Zweispaltig",
|
"sideBySide": "Zweispaltig",
|
||||||
"combined": "Kombiniert"
|
"combined": "Kombiniert",
|
||||||
|
"noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden."
|
||||||
},
|
},
|
||||||
"fileUpload": {
|
"fileUpload": {
|
||||||
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",
|
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",
|
||||||
|
|||||||
@@ -45,8 +45,7 @@
|
|||||||
"subtitle": "Erstellen eines neuen Benutzers"
|
"subtitle": "Erstellen eines neuen Benutzers"
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"subtitle": "Benutzer löschen",
|
"button": "Benutzer löschen",
|
||||||
"button": "Löschen",
|
|
||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Benutzer löschen",
|
"title": "Benutzer löschen",
|
||||||
"message": "Soll der Benutzer wirklich gelöscht werden?",
|
"message": "Soll der Benutzer wirklich gelöscht werden?",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"communityTitle": "Community Support",
|
"communityTitle": "Community Support",
|
||||||
"communityIconAlt": "Community Support Icon",
|
"communityIconAlt": "Community Support Icon",
|
||||||
"communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.",
|
"communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.",
|
||||||
"communityButton": "Contact our team",
|
"communityButton": "Contact our Team",
|
||||||
"enterpriseTitle": "Enterprise Support",
|
"enterpriseTitle": "Enterprise Support",
|
||||||
"enterpriseIconAlt": "Enterprise Support Icon",
|
"enterpriseIconAlt": "Enterprise Support Icon",
|
||||||
"enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?",
|
"enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?",
|
||||||
@@ -76,8 +76,8 @@
|
|||||||
"createSubtitle": "Create Permission Role",
|
"createSubtitle": "Create Permission Role",
|
||||||
"editSubtitle": "Edit Permission Role",
|
"editSubtitle": "Edit Permission Role",
|
||||||
"overview": {
|
"overview": {
|
||||||
"title": "Overview of all permission roles",
|
"title": "Overview of all Permission Roles",
|
||||||
"noPermissionRoles": "No permission roles found.",
|
"noPermissionRoles": "No Permission Roles found.",
|
||||||
"createButton": "Create Permission Role"
|
"createButton": "Create Permission Role"
|
||||||
},
|
},
|
||||||
"editButton": "Edit",
|
"editButton": "Edit",
|
||||||
@@ -91,10 +91,9 @@
|
|||||||
"submit": "Save"
|
"submit": "Save"
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"button": "Delete",
|
"button": "Delete Permission Role",
|
||||||
"subtitle": "Delete permission role",
|
|
||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Delete permission role",
|
"title": "Delete Permission Role",
|
||||||
"message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.",
|
"message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.",
|
||||||
"submit": "Yes",
|
"submit": "Yes",
|
||||||
"cancel": "No"
|
"cancel": "No"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user