merge + add user/group autocomplete ui-components

This commit is contained in:
Florian Scholdei
2019-06-11 18:10:29 +02:00
39 changed files with 4234 additions and 2922 deletions

14
pom.xml
View File

@@ -409,8 +409,9 @@
<plugin> <plugin>
<groupId>com.github.sdorra</groupId> <groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId> <artifactId>buildfrontend-maven-plugin</artifactId>
<version>2.2.0</version> <version>2.3.0</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
@@ -432,6 +433,12 @@
<artifactId>enunciate-maven-plugin</artifactId> <artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version> <version>${enunciate.version}</version>
</plugin> </plugin>
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-4</version>
</plugin>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
@@ -838,8 +845,8 @@
<quartz.version>2.2.3</quartz.version> <quartz.version>2.2.3</quartz.version>
<!-- frontend --> <!-- frontend -->
<nodejs.version>8.11.4</nodejs.version> <nodejs.version>10.16.0</nodejs.version>
<yarn.version>1.9.4</yarn.version> <yarn.version>1.16.0</yarn.version>
<!-- build properties --> <!-- build properties -->
<project.build.javaLevel>1.8</project.build.javaLevel> <project.build.javaLevel>1.8</project.build.javaLevel>
@@ -855,7 +862,6 @@
<!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable --> <!-- *UserPassword JS files are excluded because extraction of common code would not make the code more readable -->
<sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions> <sonar.cpd.exclusions>**/*StoreFactory.java,**/*UserPassword.js</sonar.cpd.exclusions>
<node.version>8.11.4</node.version>
<sonar.nodejs.executable>./scm-ui/target/frontend/buildfrontend-node/node-v${node.version}-linux-x64/bin/node</sonar.nodejs.executable> <sonar.nodejs.executable>./scm-ui/target/frontend/buildfrontend-node/node-v${node.version}-linux-x64/bin/node</sonar.nodejs.executable>

View File

@@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.store.StoreConstants; import sonia.scm.store.StoreConstants;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -28,6 +29,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
* *
* @since 2.0.0 * @since 2.0.0
*/ */
@Singleton
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> { public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
public static final String STORE_NAME = "repository-paths"; public static final String STORE_NAME = "repository-paths";
@@ -48,7 +50,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC()); this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
} }
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) { PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
super(Path.class); super(Path.class);
this.contextProvider = contextProvider; this.contextProvider = contextProvider;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver; this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
@@ -138,4 +140,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME) .resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION)); .resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
} }
public void refresh() {
this.read();
}
} }

View File

@@ -198,4 +198,11 @@ public class XmlRepositoryDAO implements RepositoryDAO {
public Long getLastModified() { public Long getLastModified() {
return repositoryLocationResolver.getLastModified(); return repositoryLocationResolver.getLastModified();
} }
public void refresh() {
repositoryLocationResolver.refresh();
byNamespaceAndName.clear();
byId.clear();
init();
}
} }

View File

@@ -8,8 +8,6 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory; import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
@@ -32,7 +30,9 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -47,9 +47,6 @@ class XmlRepositoryDAOTest {
@Mock @Mock
private PathBasedRepositoryLocationResolver locationResolver; private PathBasedRepositoryLocationResolver locationResolver;
@Captor
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
private FileSystem fileSystem = new DefaultFileSystem(); private FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao; private XmlRepositoryDAO dao;
@@ -268,43 +265,80 @@ class XmlRepositoryDAOTest {
verify(locationResolver).updateModificationDate(); verify(locationResolver).updateModificationDate();
} }
}
@Test private String getXmlFileContent(String id) {
void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException { Path storePath = metadataFile(id);
doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
Path repositoryPath = basePath.resolve("existing"); assertThat(storePath).isRegularFile();
Files.createDirectories(repositoryPath); return content(storePath);
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml"); }
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
forAllCaptor.getValue().accept("existing", repositoryPath); private Path metadataFile(String id) {
return locationResolver.create(id).resolve("metadata.xml");
}
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); private String content(Path storePath) {
} try {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
private String getXmlFileContent(String id) { } catch (IOException e) {
Path storePath = metadataFile(id); throw new RuntimeException(e);
}
assertThat(storePath).isRegularFile();
return content(storePath);
}
private Path metadataFile(String id) {
return locationResolver.create(id).resolve("metadata.xml");
}
private String content(Path storePath) {
try {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
} }
} }
private static Repository createRepository(String id) { @Nested
class WithExistingRepositories {
private Path repositoryPath;
@BeforeEach
void createMetadataFileForRepository(@TempDirectory.TempDir Path basePath) throws IOException {
repositoryPath = basePath.resolve("existing");
Files.createDirectories(repositoryPath);
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
}
@Test
void shouldReadExistingRepositoriesFromPathDatabase() {
// given
mockExistingPath();
// when
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
// then
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
}
@Test
void shouldRefreshWithExistingRepositoriesFromPathDatabase() {
// given
doNothing().when(locationResolver).forAllPaths(any());
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
mockExistingPath();
// when
dao.refresh();
// then
verify(locationResolver).refresh();
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
}
private void mockExistingPath() {
doAnswer(
invocation -> {
((BiConsumer<String, Path>) invocation.getArgument(0)).accept("existing", repositoryPath);
return null;
}
).when(locationResolver).forAllPaths(any());
}
}
private Repository createRepository(String id) {
return new Repository(id, "xml", "space", id); return new Repository(id, "xml", "space", id);
} }
} }

View File

@@ -112,7 +112,6 @@
<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-3</version>
<extensions>true</extensions> <extensions>true</extensions>
</plugin> </plugin>

View File

@@ -12,6 +12,6 @@
"@scm-manager/ui-extensions": "^0.1.2" "@scm-manager/ui-extensions": "^0.1.2"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28" "@scm-manager/ui-bundler": "^0.0.29"
} }
} }

View File

@@ -1,97 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.github.legman.Subscribe;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.EagerSingleton;
import sonia.scm.HandlerEventType;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
/**
* Listener which removes all lfs objects from a blob store, whenever its corresponding git repository gets deleted.
*
* @author Sebastian Sdorra
* @since 1.54
*/
@Extension
@EagerSingleton
public class LfsStoreRemoveListener {
private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreFactory.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
@Inject
public LfsStoreRemoveListener(LfsBlobStoreFactory lfsBlobStoreFactory) {
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
}
/**
* Remove all object from the blob store, if the event is an delete event and the repository is a git repository.
*
* @param event repository event
*/
@Subscribe
public void handleRepositoryEvent(RepositoryEvent event) {
if ( isDeleteEvent(event) && isGitRepositoryEvent(event) ) {
removeLfsStore(event.getItem());
}
}
private boolean isDeleteEvent(RepositoryEvent event) {
return HandlerEventType.DELETE == event.getEventType();
}
private boolean isGitRepositoryEvent(RepositoryEvent event) {
return event.getItem() != null
&& event.getItem().getType().equals(GitRepositoryHandler.TYPE_NAME);
}
private void removeLfsStore(Repository repository) {
LOG.debug("remove all blobs from store, because corresponding git repository {} was removed", repository.getName());
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
for ( Blob blob : blobStore.getAll() ) {
LOG.trace("remove blob {}, because repository {} was removed", blob.getId(), repository.getName());
blobStore.remove(blob);
}
}
}

View File

@@ -1,122 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.web.lfs;
import com.google.common.collect.Lists;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.*;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.HandlerEventType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
/**
* Unit tests for {@link LfsStoreRemoveListener}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class LfsStoreRemoveListenerTest {
@Mock
private LfsBlobStoreFactory lfsBlobStoreFactory;
@Mock
private BlobStore blobStore;
@InjectMocks
private LfsStoreRemoveListener lfsStoreRemoveListener;
@Test
public void testHandleRepositoryEventWithNonDeleteEvents() {
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_CREATE));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.CREATE));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_MODIFY));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.MODIFY));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.BEFORE_DELETE));
verifyZeroInteractions(lfsBlobStoreFactory);
}
@Test
public void testHandleRepositoryEventWithNonGitRepositories() {
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "svn"));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "hg"));
lfsStoreRemoveListener.handleRepositoryEvent(event(HandlerEventType.DELETE, "dummy"));
verifyZeroInteractions(lfsBlobStoreFactory);
}
@Test
public void testHandleRepositoryEvent() {
Repository heartOfGold = RepositoryTestData.createHeartOfGold("git");
when(lfsBlobStoreFactory.getLfsBlobStore(heartOfGold)).thenReturn(blobStore);
Blob blobA = mockBlob("a");
Blob blobB = mockBlob("b");
List<Blob> blobs = Lists.newArrayList(blobA, blobB);
when(blobStore.getAll()).thenReturn(blobs);
lfsStoreRemoveListener.handleRepositoryEvent(new RepositoryEvent(HandlerEventType.DELETE, heartOfGold));
verify(blobStore).getAll();
verify(blobStore).remove(blobA);
verify(blobStore).remove(blobB);
verifyNoMoreInteractions(blobStore);
}
private Blob mockBlob(String id) {
Blob blob = mock(Blob.class);
when(blob.getId()).thenReturn(id);
return blob;
}
private RepositoryEvent event(HandlerEventType eventType) {
return event(eventType, "git");
}
private RepositoryEvent event(HandlerEventType eventType, String repositoryType) {
return new RepositoryEvent(eventType, RepositoryTestData.create42Puzzle(repositoryType));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "@scm-manager/scm-hg-plugin", "name": "@scm-manager/scm-hg-plugin",
"main": "src/main/js/index.js", "main": "src/main/js/index.js",
"license" : "BSD-3-Clause", "license": "BSD-3-Clause",
"scripts": { "scripts": {
"build": "ui-bundler plugin" "build": "ui-bundler plugin"
}, },
@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2" "@scm-manager/ui-extensions": "^0.1.2"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28" "@scm-manager/ui-bundler": "^0.0.29"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2" "@scm-manager/ui-extensions": "^0.1.2"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28" "@scm-manager/ui-bundler": "^0.0.29"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"eslint-fix": "eslint src --fix" "eslint-fix": "eslint src --fix"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28", "@scm-manager/ui-bundler": "^0.0.29",
"create-index": "^2.3.0", "create-index": "^2.3.0",
"enzyme": "^3.5.0", "enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1", "enzyme-adapter-react-16": "^1.3.1",

View File

@@ -4,7 +4,6 @@ import { AsyncCreatable, Async } from "react-select";
import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
type Props = { type Props = {
loadSuggestions: string => Promise<AutocompleteObject>, loadSuggestions: string => Promise<AutocompleteObject>,
valueSelected: SelectValue => void, valueSelected: SelectValue => void,

View File

@@ -10,35 +10,33 @@ type Props = {
error?: Error error?: Error
}; };
class ErrorNotification extends React.Component<Props> { class ErrorNotification extends React.Component<Props> {
render() { render() {
const { t, error } = this.props; const { t, error } = this.props;
if (error) { if (error) {
if (error instanceof BackendError) { if (error instanceof BackendError) {
return <BackendErrorNotification error={error} /> return <BackendErrorNotification error={error} />;
} else if (error instanceof UnauthorizedError) { } else if (error instanceof UnauthorizedError) {
return ( return (
<Notification type="danger"> <Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong>{" "} <strong>{t("errorNotification.prefix")}:</strong>{" "}
{t("error-notification.timeout")}{" "} {t("errorNotification.timeout")}{" "}
<a href="javascript:window.location.reload(true)"> <a href="javascript:window.location.reload(true)">
{t("error-notification.loginLink")} {t("errorNotification.loginLink")}
</a> </a>
</Notification> </Notification>
); );
} else if (error instanceof ForbiddenError) { } else if (error instanceof ForbiddenError) {
return ( return (
<Notification type="danger"> <Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong>{" "} <strong>{t("errorNotification.prefix")}:</strong>{" "}
{t("error-notification.forbidden")} {t("errorNotification.forbidden")}
</Notification> </Notification>
) );
} else } else {
{
return ( return (
<Notification type="danger"> <Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong> {error.message} <strong>{t("errorNotification.prefix")}:</strong> {error.message}
</Notification> </Notification>
); );
} }
@@ -47,4 +45,4 @@ class ErrorNotification extends React.Component<Props> {
} }
} }
export default translate("commons")(ErrorNotification); export default translate("commons")(ErrorNotification);

View File

@@ -0,0 +1,37 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { SelectValue } from "@scm-manager/ui-types";
import UserGroupAutocomplete from "./UserGroupAutocomplete";
type Props = {
groupAutocompleteLink: string,
valueSelected: SelectValue => void,
value: string,
// Context props
t: string => string
};
class GroupAutocomplete extends React.Component<Props> {
selectName = (selection: SelectValue) => {
this.props.valueSelected(selection);
};
render() {
const { groupAutocompleteLink, t, value } = this.props;
return (
<UserGroupAutocomplete
autocompleteLink={groupAutocompleteLink}
label={t("autocomplete.group")}
noOptionsMessage={t("autocomplete.noGroupOptions")}
loadingMessage={t("autocomplete.loading")}
placeholder={t("autocomplete.groupPlaceholder")}
valueSelected={this.selectName}
value={value}
/>
);
}
}
export default translate("commons")(GroupAutocomplete);

View File

@@ -0,0 +1,37 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { SelectValue } from "@scm-manager/ui-types";
import UserGroupAutocomplete from "./UserGroupAutocomplete";
type Props = {
userAutocompleteLink: string,
valueSelected: SelectValue => void,
value: string,
// Context props
t: string => string
};
class UserAutocomplete extends React.Component<Props> {
selectName = (selection: SelectValue) => {
this.props.valueSelected(selection);
};
render() {
const { userAutocompleteLink, t, value } = this.props;
return (
<UserGroupAutocomplete
autocompleteLink={userAutocompleteLink}
label={t("autocomplete.user")}
noOptionsMessage={t("autocomplete.noUserOptions")}
loadingMessage={t("autocomplete.loading")}
placeholder={t("autocomplete.userPlaceholder")}
valueSelected={this.selectName}
value={value}
/>
);
}
}
export default translate("commons")(UserAutocomplete);

View File

@@ -1,21 +1,18 @@
// @flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next";
import { Autocomplete } from "@scm-manager/ui-components";
import type { SelectValue } from "@scm-manager/ui-types"; import type { SelectValue } from "@scm-manager/ui-types";
import Autocomplete from "./Autocomplete";
type Props = { type Props = {
userAutocompleteLink: string, autocompleteLink: string,
valueSelected: SelectValue => void, valueSelected: SelectValue => void,
value: string, value: string,
label: string
// Context props
t: string => string
}; };
class UserAutocomplete extends React.Component<Props> { class UserGroupAutocomplete extends React.Component<Props> {
loadUserSuggestions = (inputValue: string) => { loadSuggestions = (inputValue: string) => {
const url = this.props.userAutocompleteLink; const url = this.props.autocompleteLink;
const link = url + "?q="; const link = url + "?q=";
return fetch(link + inputValue) return fetch(link + inputValue)
.then(response => response.json()) .then(response => response.json())
@@ -37,20 +34,18 @@ class UserAutocomplete extends React.Component<Props> {
}; };
render() { render() {
const { t, value } = this.props; const { value, label } = this.props;
return ( return (
<Autocomplete <Autocomplete
loadSuggestions={this.loadUserSuggestions} loadSuggestions={this.loadSuggestions}
label={t("permission.user")}
noOptionsMessage={t("permission.autocomplete.no-user-options")}
loadingMessage={t("permission.autocomplete.loading")}
placeholder={t("permission.autocomplete.user-placeholder")}
valueSelected={this.selectName} valueSelected={this.selectName}
value={value} value={value}
creatable={true} creatable={true}
label={label}
{...this.props}
/> />
); );
} }
} }
export default translate("repos")(UserAutocomplete); export default UserGroupAutocomplete;

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"check": "flow check" "check": "flow check"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28" "@scm-manager/ui-bundler": "^0.0.29"
}, },
"browserify": { "browserify": {
"transform": [ "transform": [

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,7 @@
"pre-commit": "jest && flow && eslint src" "pre-commit": "jest && flow && eslint src"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28", "@scm-manager/ui-bundler": "^0.0.29",
"concat": "^1.0.3", "concat": "^1.0.3",
"copyfiles": "^2.0.0", "copyfiles": "^2.0.0",
"enzyme": "^3.3.0", "enzyme": "^3.3.0",

View File

@@ -19,11 +19,11 @@
"subtitle": "Ein unbekannter Fehler ist aufgetreten." "subtitle": "Ein unbekannter Fehler ist aufgetreten."
} }
}, },
"error-notification": { "errorNotification": {
"prefix": "Fehler", "prefix": "Fehler",
"loginLink": "Erneute Anmeldung", "loginLink": "Erneute Anmeldung",
"timeout": "Die Session ist abgelaufen.", "timeout": "Die Session ist abgelaufen.",
"wrong-login-credentials": "Ungültige Anmeldedaten", "wrongLoginCredentials": "Ungültige Anmeldedaten",
"forbidden": "Sie haben nicht die Berechtigung, diesen Datensatz zu sehen" "forbidden": "Sie haben nicht die Berechtigung, diesen Datensatz zu sehen"
}, },
"loading": { "loading": {
@@ -40,6 +40,15 @@
"config": "Einstellungen" "config": "Einstellungen"
}, },
"filterEntries": "Einträge filtern", "filterEntries": "Einträge filtern",
"autocomplete": {
"group": "Gruppe",
"user": "Benutzer",
"no-group-options": "Kein Gruppenname als Vorschlag verfügbar",
"group-placeholder": "Gruppe eingeben",
"no-user-options": "Kein Benutzername als Vorschlag verfügbar",
"user-placeholder": "Benutzer eingeben",
"loading": "suche..."
},
"paginator": { "paginator": {
"next": "Weiter", "next": "Weiter",
"previous": "Zurück" "previous": "Zurück"

View File

@@ -150,13 +150,6 @@
"roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird, den Erweitert-Button benutzen, um Details zu sehen.", "roleHelpText": "READ = read; WRITE = read und write; OWNER = read, write und auch die Möglichkeit Einstellungen und Berechtigungen zu verwalten. Wenn hier nichts angezeigt wird, den Erweitert-Button benutzen, um Details zu sehen.",
"permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden." "permissionsHelpText": "Hier können individuelle Berechtigungen unabhängig von vordefinierten Rollen vergeben werden."
}, },
"autocomplete": {
"no-group-options": "Kein Gruppenname als Vorschlag verfügbar",
"group-placeholder": "Gruppe eingeben",
"no-user-options": "Kein Benutzername als Vorschlag verfügbar",
"user-placeholder": "Benutzer eingeben",
"loading": "suche..."
},
"advanced": { "advanced": {
"dialog": { "dialog": {
"title": "Erweiterte Berechtigungen", "title": "Erweiterte Berechtigungen",

View File

@@ -19,11 +19,11 @@
"subtitle": "Unknown error occurred" "subtitle": "Unknown error occurred"
} }
}, },
"error-notification": { "errorNotification": {
"prefix": "Error", "prefix": "Error",
"loginLink": "You can login here again.", "loginLink": "You can login here again.",
"timeout": "The session has expired", "timeout": "The session has expired",
"wrong-login-credentials": "Invalid credentials", "wrongLoginCredentials": "Invalid credentials",
"forbidden": "You don't have permission to view this entity" "forbidden": "You don't have permission to view this entity"
}, },
"loading": { "loading": {
@@ -40,6 +40,15 @@
"config": "Configuration" "config": "Configuration"
}, },
"filterEntries": "filter entries", "filterEntries": "filter entries",
"autocomplete": {
"group": "Group",
"user": "User",
"noGroupOptions": "No group suggestion available",
"groupPlaceholder": "Enter group",
"noUserOptions": "No user suggestion available",
"userPlaceholder": "Enter user",
"loading": "Loading..."
},
"paginator": { "paginator": {
"next": "Next", "next": "Next",
"previous": "Previous" "previous": "Previous"

View File

@@ -153,13 +153,6 @@
"roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.", "roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.",
"permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles." "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles."
}, },
"autocomplete": {
"no-group-options": "No group suggestion available",
"group-placeholder": "Enter group",
"no-user-options": "No user suggestion available",
"user-placeholder": "Enter user",
"loading": "Loading..."
},
"advanced": { "advanced": {
"dialog": { "dialog": {
"title": "Advanced Permissions", "title": "Advanced Permissions",

View File

@@ -95,7 +95,7 @@ class Login extends React.Component<Props, State> {
areCredentialsInvalid() { areCredentialsInvalid() {
const { t, error } = this.props; const { t, error } = this.props;
if (error instanceof UnauthorizedError) { if (error instanceof UnauthorizedError) {
return new Error(t("error-notification.wrong-login-credentials")); return new Error(t("errorNotification.wrongLoginCredentials"));
} else { } else {
return error; return error;
} }

View File

@@ -1,56 +0,0 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Autocomplete } from "@scm-manager/ui-components";
import type { SelectValue } from "@scm-manager/ui-types";
type Props = {
groupAutocompleteLink: string,
valueSelected: SelectValue => void,
value: string,
// Context props
t: string => string
};
class GroupAutocomplete extends React.Component<Props> {
loadGroupSuggestions = (inputValue: string) => {
const url = this.props.groupAutocompleteLink;
const link = url + "?q=";
return fetch(link + inputValue)
.then(response => response.json())
.then(json => {
return json.map(element => {
const label = element.displayName
? `${element.displayName} (${element.id})`
: element.id;
return {
value: element,
label
};
});
});
};
selectName = (selection: SelectValue) => {
this.props.valueSelected(selection);
};
render() {
const { t, value } = this.props;
return (
<Autocomplete
loadSuggestions={this.loadGroupSuggestions}
label={t("permission.group")}
noOptionsMessage={t("permission.autocomplete.no-group-options")}
loadingMessage={t("permission.autocomplete.loading")}
placeholder={t("permission.autocomplete.group-placeholder")}
valueSelected={this.selectName}
value={value}
creatable={true}
/>
);
}
}
export default translate("repos")(GroupAutocomplete);

View File

@@ -12,12 +12,12 @@ import {
SubmitButton, SubmitButton,
Button, Button,
LabelWithHelpIcon, LabelWithHelpIcon,
Radio Radio,
GroupAutocomplete,
UserAutocomplete
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import * as validator from "../components/permissionValidation"; import * as validator from "../components/permissionValidation";
import RoleSelector from "../components/RoleSelector"; import RoleSelector from "../components/RoleSelector";
import GroupAutocomplete from "../components/GroupAutocomplete";
import UserAutocomplete from "../components/UserAutocomplete";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
import { findVerbsForRole } from "../modules/permissions"; import { findVerbsForRole } from "../modules/permissions";

File diff suppressed because it is too large Load Diff

View File

@@ -461,7 +461,6 @@
<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-2</version>
<configuration> <configuration>
<artifactItems> <artifactItems>
<artifactItem> <artifactItem>

View File

@@ -63,7 +63,9 @@ public class XmlGroupV1UpdateStep implements UpdateStep {
return; return;
} }
XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get()); XmlGroupV1UpdateStep.V1GroupDatabase v1Database = readV1Database(v1GroupsFile.get());
v1Database.groupList.groups.forEach(this::update); if (v1Database.groupList != null && v1Database.groupList.groups != null) {
v1Database.groupList.groups.forEach(this::update);
}
} }
@Override @Override

View File

@@ -6,6 +6,7 @@ import sonia.scm.SCMContextProvider;
import sonia.scm.migration.UpdateStep; import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import sonia.scm.store.StoreConstants; import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version; import sonia.scm.version.Version;
@@ -27,10 +28,12 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class); private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryFileNameUpdateStep.class);
private final SCMContextProvider contextProvider; private final SCMContextProvider contextProvider;
private final XmlRepositoryDAO repositoryDAO;
@Inject @Inject
public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider) { public XmlRepositoryFileNameUpdateStep(SCMContextProvider contextProvider, XmlRepositoryDAO repositoryDAO) {
this.contextProvider = contextProvider; this.contextProvider = contextProvider;
this.repositoryDAO = repositoryDAO;
} }
@Override @Override
@@ -41,6 +44,7 @@ public class XmlRepositoryFileNameUpdateStep implements UpdateStep {
if (Files.exists(oldRepositoriesFile)) { if (Files.exists(oldRepositoriesFile)) {
LOG.info("moving old repositories database files to repository-paths file"); LOG.info("moving old repositories database files to repository-paths file");
Files.move(oldRepositoriesFile, newRepositoryPathsFile); Files.move(oldRepositoriesFile, newRepositoryPathsFile);
repositoryDAO.refresh();
} }
} }

View File

@@ -47,6 +47,12 @@ private final SCMContextProvider contextProvider;
copyTestDatabaseFile(configDir, fileName); copyTestDatabaseFile(configDir, fileName);
} }
public void copyConfigFile(String fileName, String targetFileName) throws IOException {
Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir);
copyTestDatabaseFile(configDir, fileName, targetFileName);
}
public ConfigurationEntryStore<AssignedPermission> getStoreForConfigFile(String name) { public ConfigurationEntryStore<AssignedPermission> getStoreForConfigFile(String name) {
return storeFactory return storeFactory
.withType(AssignedPermission.class) .withType(AssignedPermission.class)
@@ -59,7 +65,12 @@ private final SCMContextProvider contextProvider;
} }
private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException { private void copyTestDatabaseFile(Path configDir, String fileName) throws IOException {
Path targetFileName = Paths.get(fileName).getFileName();
copyTestDatabaseFile(configDir, fileName, targetFileName.toString());
}
private void copyTestDatabaseFile(Path configDir, String fileName, String targetFileName) throws IOException {
URL url = Resources.getResource(fileName); URL url = Resources.getResource(fileName);
Files.copy(url.openStream(), configDir.resolve(Paths.get(fileName).getFileName())); Files.copy(url.openStream(), configDir.resolve(targetFileName));
} }
} }

View File

@@ -99,6 +99,36 @@ class XmlGroupV1UpdateStepTest {
} }
} }
@Nested
class WithExistingDatabaseWithEmptyList {
@BeforeEach
void createGroupV1XML() throws IOException {
testUtil.copyConfigFile("sonia/scm/update/group/groups_empty_groups.xml", "groups.xml");
}
@Test
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
updateStep.doUpdate();
verify(groupDAO, times(0)).add(any());
}
}
@Nested
class WithExistingDatabaseWithoutList {
@BeforeEach
void createGroupV1XML() throws IOException {
testUtil.copyConfigFile("sonia/scm/update/group/groups_no_groups.xml", "groups.xml");
}
@Test
void shouldCreateNewGroupFromGroupsV1Xml() throws JAXBException {
updateStep.doUpdate();
verify(groupDAO, times(0)).add(any());
}
}
@Test @Test
void shouldNotFailForMissingConfigDir() throws JAXBException { void shouldNotFailForMissingConfigDir() throws JAXBException {
updateStep.doUpdate(); updateStep.doUpdate();

View File

@@ -7,8 +7,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory; import org.junitpioneer.jupiter.TempDirectory;
import sonia.scm.SCMContextProvider; import sonia.scm.SCMContextProvider;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.repository.xml.XmlRepositoryDAO;
import javax.xml.bind.JAXBException;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
@@ -16,12 +16,14 @@ import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class) @ExtendWith(TempDirectory.class)
class XmlRepositoryFileNameUpdateStepTest { class XmlRepositoryFileNameUpdateStepTest {
SCMContextProvider contextProvider = mock(SCMContextProvider.class); SCMContextProvider contextProvider = mock(SCMContextProvider.class);
XmlRepositoryDAO repositoryDAO = mock(XmlRepositoryDAO.class);
@BeforeEach @BeforeEach
void mockScmHome(@TempDirectory.TempDir Path tempDir) { void mockScmHome(@TempDirectory.TempDir Path tempDir) {
@@ -29,8 +31,8 @@ class XmlRepositoryFileNameUpdateStepTest {
} }
@Test @Test
void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws JAXBException, IOException { void shouldCopyRepositoriesFileToRepositoryPathsFile(@TempDirectory.TempDir Path tempDir) throws IOException {
XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider); XmlRepositoryFileNameUpdateStep updateStep = new XmlRepositoryFileNameUpdateStep(contextProvider, repositoryDAO);
URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml"); URL url = Resources.getResource("sonia/scm/update/repository/formerV2RepositoryFile.xml");
Path configDir = tempDir.resolve("config"); Path configDir = tempDir.resolve("config");
Files.createDirectories(configDir); Files.createDirectories(configDir);
@@ -40,5 +42,6 @@ class XmlRepositoryFileNameUpdateStepTest {
assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists(); assertThat(configDir.resolve(PathBasedRepositoryLocationResolver.STORE_NAME + ".xml")).exists();
assertThat(configDir.resolve("repositories.xml")).doesNotExist(); assertThat(configDir.resolve("repositories.xml")).doesNotExist();
verify(repositoryDAO).refresh();
} }
} }

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<group-db>
<creationTime>1558006904769</creationTime>
<groups/>
<lastModified>1558007174172</lastModified>
</group-db>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<group-db>
<creationTime>1558006904769</creationTime>
<lastModified>1558007174172</lastModified>
</group-db>