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>
<groupId>com.github.sdorra</groupId>
<artifactId>buildfrontend-maven-plugin</artifactId>
<version>2.2.0</version>
<version>2.3.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
@@ -432,6 +433,12 @@
<artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version>
</plugin>
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-4</version>
</plugin>
</plugins>
</pluginManagement>
@@ -838,8 +845,8 @@
<quartz.version>2.2.3</quartz.version>
<!-- frontend -->
<nodejs.version>8.11.4</nodejs.version>
<yarn.version>1.9.4</yarn.version>
<nodejs.version>10.16.0</nodejs.version>
<yarn.version>1.16.0</yarn.version>
<!-- build properties -->
<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 -->
<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>

View File

@@ -7,6 +7,7 @@ import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.store.StoreConstants;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -28,6 +29,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
*
* @since 2.0.0
*/
@Singleton
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
public static final String STORE_NAME = "repository-paths";
@@ -48,7 +50,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
}
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
super(Path.class);
this.contextProvider = contextProvider;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
@@ -138,4 +140,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.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() {
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.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -32,7 +30,9 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -47,9 +47,6 @@ class XmlRepositoryDAOTest {
@Mock
private PathBasedRepositoryLocationResolver locationResolver;
@Captor
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
private FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao;
@@ -268,22 +265,6 @@ class XmlRepositoryDAOTest {
verify(locationResolver).updateModificationDate();
}
}
@Test
void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException {
doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
Path repositoryPath = basePath.resolve("existing");
Files.createDirectories(repositoryPath);
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
forAllCaptor.getValue().accept("existing", repositoryPath);
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
}
private String getXmlFileContent(String id) {
Path storePath = metadataFile(id);
@@ -303,8 +284,61 @@ class XmlRepositoryDAOTest {
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);
}
}

View File

@@ -112,7 +112,6 @@
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-3</version>
<extensions>true</extensions>
</plugin>

View File

@@ -12,6 +12,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"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

@@ -9,6 +9,6 @@
"@scm-manager/ui-extensions": "^0.1.2"
},
"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"
},
"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"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28",
"@scm-manager/ui-bundler": "^0.0.29",
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"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 LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
type Props = {
loadSuggestions: string => Promise<AutocompleteObject>,
valueSelected: SelectValue => void,

View File

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

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
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 Autocomplete from "./Autocomplete";
type Props = {
userAutocompleteLink: string,
autocompleteLink: string,
valueSelected: SelectValue => void,
value: string,
// Context props
t: string => string
label: string
};
class UserAutocomplete extends React.Component<Props> {
loadUserSuggestions = (inputValue: string) => {
const url = this.props.userAutocompleteLink;
class UserGroupAutocomplete extends React.Component<Props> {
loadSuggestions = (inputValue: string) => {
const url = this.props.autocompleteLink;
const link = url + "?q=";
return fetch(link + inputValue)
.then(response => response.json())
@@ -37,20 +34,18 @@ class UserAutocomplete extends React.Component<Props> {
};
render() {
const { t, value } = this.props;
const { value, label } = this.props;
return (
<Autocomplete
loadSuggestions={this.loadUserSuggestions}
label={t("permission.user")}
noOptionsMessage={t("permission.autocomplete.no-user-options")}
loadingMessage={t("permission.autocomplete.loading")}
placeholder={t("permission.autocomplete.user-placeholder")}
loadSuggestions={this.loadSuggestions}
valueSelected={this.selectName}
value={value}
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"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.28"
"@scm-manager/ui-bundler": "^0.0.29"
},
"browserify": {
"transform": [

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -19,11 +19,11 @@
"subtitle": "Ein unbekannter Fehler ist aufgetreten."
}
},
"error-notification": {
"errorNotification": {
"prefix": "Fehler",
"loginLink": "Erneute Anmeldung",
"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"
},
"loading": {
@@ -40,6 +40,15 @@
"config": "Einstellungen"
},
"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": {
"next": "Weiter",
"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.",
"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": {
"dialog": {
"title": "Erweiterte Berechtigungen",

View File

@@ -19,11 +19,11 @@
"subtitle": "Unknown error occurred"
}
},
"error-notification": {
"errorNotification": {
"prefix": "Error",
"loginLink": "You can login here again.",
"timeout": "The session has expired",
"wrong-login-credentials": "Invalid credentials",
"wrongLoginCredentials": "Invalid credentials",
"forbidden": "You don't have permission to view this entity"
},
"loading": {
@@ -40,6 +40,15 @@
"config": "Configuration"
},
"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": {
"next": "Next",
"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.",
"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": {
"dialog": {
"title": "Advanced Permissions",

View File

@@ -95,7 +95,7 @@ class Login extends React.Component<Props, State> {
areCredentialsInvalid() {
const { t, error } = this.props;
if (error instanceof UnauthorizedError) {
return new Error(t("error-notification.wrong-login-credentials"));
return new Error(t("errorNotification.wrongLoginCredentials"));
} else {
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,
Button,
LabelWithHelpIcon,
Radio
Radio,
GroupAutocomplete,
UserAutocomplete
} from "@scm-manager/ui-components";
import * as validator from "../components/permissionValidation";
import RoleSelector from "../components/RoleSelector";
import GroupAutocomplete from "../components/GroupAutocomplete";
import UserAutocomplete from "../components/UserAutocomplete";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
import { findVerbsForRole } from "../modules/permissions";

File diff suppressed because it is too large Load Diff

View File

@@ -461,7 +461,6 @@
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-alpha-2</version>
<configuration>
<artifactItems>
<artifactItem>

View File

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

View File

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

View File

@@ -47,6 +47,12 @@ private final SCMContextProvider contextProvider;
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) {
return storeFactory
.withType(AssignedPermission.class)
@@ -59,7 +65,12 @@ private final SCMContextProvider contextProvider;
}
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);
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
void shouldNotFailForMissingConfigDir() throws JAXBException {
updateStep.doUpdate();

View File

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