mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-07 08:02:09 +01:00
Add ConfigurationAdapterBase and extension points for trash bin
Adds the new abstract class ConfigurationAdapterBase to simplify the creation of global configuration views. In addition there is some cleanup, interfaces and extension points for the repository trash bin plugin. Committed-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com> Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@@ -88,7 +88,7 @@ pipeline {
|
||||
sh 'git fetch origin develop'
|
||||
script {
|
||||
withSonarQubeEnv('sonarcloud.io-scm') {
|
||||
String parameters = ' -Dsonar.organization=scm-manager'
|
||||
String parameters = ' -Dsonar.organization=scm-manager -Dsonar.analysis.scmm-repo=scm-manager/scm-manager'
|
||||
if (env.CHANGE_ID) {
|
||||
parameters += ' -Dsonar.pullrequest.provider=GitHub'
|
||||
parameters += ' -Dsonar.pullrequest.github.repository=scm-manager/scm-manager'
|
||||
|
||||
2
gradle/changelog/abstract_config_adapter.yaml
Normal file
2
gradle/changelog/abstract_config_adapter.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Add abstract configuration adapter to simply creating new global configurations
|
||||
2
gradle/changelog/correct_revision_to_merge.yaml
Normal file
2
gradle/changelog/correct_revision_to_merge.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: The 'revision to merge' in merge results
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.github.sdorra.ssp.PermissionCheck;
|
||||
import com.google.common.annotations.Beta;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
* This can be used as a base class for configuration resources.
|
||||
*
|
||||
* @param <DAO> The class of the data access object used to persist the configuration.
|
||||
* @param <DTO> The class of the data transfer objects used in the rest api.
|
||||
* @since 2.41.0
|
||||
*/
|
||||
@Beta
|
||||
@Slf4j
|
||||
public abstract class ConfigurationAdapterBase<DAO, DTO extends HalRepresentation> implements HalEnricher {
|
||||
|
||||
protected final ConfigurationStoreFactory configurationStoreFactory;
|
||||
|
||||
protected final Provider<ScmPathInfoStore> scmPathInfoStoreProvider;
|
||||
|
||||
private final Class<DAO> daoClass;
|
||||
private final DtoToDaoMapper<DTO, DAO> dtoToDaoMapper;
|
||||
private final DaoToDtoMapper<DAO, DTO> daoToDtoMapper;
|
||||
|
||||
/**
|
||||
* Creates the resource. To do so, you have to provide the {@link ConfigurationStoreFactory} and
|
||||
* the {@link ScmPathInfoStore} as a {@link Provider} and implementations for
|
||||
* the DAO and the DTO.
|
||||
* <br>
|
||||
* The DAO class has to have a default constructor that creates the default (initial)
|
||||
* configuration.
|
||||
* <br>
|
||||
* The DTO class should be created with <code>@GenerateDto</code> annotation using Conveyor.
|
||||
* If the implementation is done manually, it has to provide two methods:
|
||||
* <ul>
|
||||
* <li>A static method <code>DTO from(DAO, Links)</code> creating the DTO instance
|
||||
* for the given DAO with the provided links.</li>
|
||||
* <li>A method <code>DAO toEntity()</code> creating the DAO from the DTO.</li>
|
||||
* </ul>
|
||||
* If either one is missing, you will see {@link IllegalDaoClassException}s on your way.
|
||||
* <br>
|
||||
* The implementation may look like this:
|
||||
* <pre>
|
||||
* @Path("/v2/test")
|
||||
* private static class TestConfigurationAdapter extends ConfigurationAdapterBase<TestConfiguration, TestConfigurationDto> {
|
||||
*
|
||||
* @Inject
|
||||
* public TestConfigurationResource(ConfigurationStoreFactory configurationStoreFactory, Provider<ScmPathInfoStore> scmPathInfoStoreProvider) {
|
||||
* super(configurationStoreFactory, scmPathInfoStoreProvider, TestConfiguration.class, TestConfigurationDto.class);
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected String getName() {
|
||||
* return "testConfig";
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param configurationStoreFactory The configuration store factory provided from injection.
|
||||
* @param scmPathInfoStoreProvider The path info store provider provided from injection.
|
||||
* @param daoClass The DAO class instance.
|
||||
* @param dtoClass The DTO class instance.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected ConfigurationAdapterBase(ConfigurationStoreFactory configurationStoreFactory,
|
||||
Provider<ScmPathInfoStore> scmPathInfoStoreProvider,
|
||||
Class<DAO> daoClass,
|
||||
Class<DTO> dtoClass) {
|
||||
this.configurationStoreFactory = configurationStoreFactory;
|
||||
this.scmPathInfoStoreProvider = scmPathInfoStoreProvider;
|
||||
this.daoClass = daoClass;
|
||||
this.dtoToDaoMapper = (DTO dto) -> {
|
||||
try {
|
||||
return (DAO) dtoClass.getDeclaredMethod("toEntity").invoke(dto);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalDtoClassException(e);
|
||||
}
|
||||
};
|
||||
this.daoToDtoMapper = (DAO entity, Links.Builder linkBuilder) -> {
|
||||
try {
|
||||
return (DTO) dtoClass.getMethod("from", daoClass, Links.class)
|
||||
.invoke(null, entity, linkBuilder.build());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalDtoClassException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public DAO getConfiguration() {
|
||||
return getConfigStore().getOptional().orElse(createDefaultDaoInstance());
|
||||
}
|
||||
|
||||
protected abstract String getName();
|
||||
|
||||
protected String getStoreName() {
|
||||
return toKebap(getName());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("")
|
||||
public DTO get(@Context UriInfo uriInfo) {
|
||||
getReadPermission().check();
|
||||
return daoToDtoMapper.mapDaoToDto(getConfiguration(), createDtoLinks());
|
||||
}
|
||||
|
||||
private DAO createDefaultDaoInstance() {
|
||||
try {
|
||||
return daoClass.getConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalDaoClassException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigurationStore<DAO> getConfigStore() {
|
||||
return configurationStoreFactory.withType(daoClass).withName(toKebap(getName())).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Path("")
|
||||
public void update(@NotNull @Valid DTO payload) {
|
||||
getWritePermission().check();
|
||||
getConfigStore().set(dtoToDaoMapper.mapDtoToDao(payload));
|
||||
}
|
||||
|
||||
private Links.Builder createDtoLinks() {
|
||||
Links.Builder builder = Links.linkingTo();
|
||||
builder.single(Link.link("self", getReadLink()));
|
||||
if (getWritePermission().isPermitted()) {
|
||||
builder.single(Link.link("update", getUpdateLink()));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private String getReadLink() {
|
||||
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStoreProvider.get().get(), this.getClass());
|
||||
return linkBuilder.method("get").parameters().href();
|
||||
}
|
||||
|
||||
private String getUpdateLink() {
|
||||
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStoreProvider.get().get(), this.getClass());
|
||||
return linkBuilder.method("update").parameters().href();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void enrich(HalEnricherContext context, HalAppender appender) {
|
||||
if (getReadPermission().isPermitted()) {
|
||||
appender.appendLink(getName(), getReadLink());
|
||||
}
|
||||
}
|
||||
|
||||
protected PermissionCheck getReadPermission() {
|
||||
return ConfigurationPermissions.read(getName());
|
||||
}
|
||||
|
||||
protected PermissionCheck getWritePermission() {
|
||||
return ConfigurationPermissions.write(getName());
|
||||
}
|
||||
|
||||
private static class IllegalDtoClassException extends RuntimeException {
|
||||
public IllegalDtoClassException(Throwable cause) {
|
||||
super("Missing method #from(DAO, Links) or #toEntity() in DTO class; see JavaDoc for ConfigurationResourceBase", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static class IllegalDaoClassException extends RuntimeException {
|
||||
public IllegalDaoClassException(Throwable cause) {
|
||||
super("Missing default constructor in DAO class; see JavaDoc for ConfigurationResourceBase", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private String toKebap(String other) {
|
||||
return other.replaceAll("([a-z])([A-Z]+)", "$1-$2").toLowerCase();
|
||||
}
|
||||
|
||||
private interface DaoToDtoMapper<DAO, DTO> {
|
||||
DTO mapDaoToDto(DAO entity, Links.Builder linkBuilder);
|
||||
}
|
||||
|
||||
private interface DtoToDaoMapper<DTO, DAO> {
|
||||
DAO mapDtoToDao(DTO dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface FullRepositoryExporter {
|
||||
void export(Repository repository, OutputStream outputStream, String password);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface FullRepositoryImporter {
|
||||
Repository importFromStream(Repository repository, InputStream inputStream, String password);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public class GitMergeRebase extends GitMergeStrategy {
|
||||
.include(branchToMerge, sourceRevision)
|
||||
.call();
|
||||
push();
|
||||
return MergeCommandResult.success(getTargetRevision().name(), branchToMerge, sourceRevision.name());
|
||||
return createSuccessResult(sourceRevision.name());
|
||||
} catch (GitAPIException e) {
|
||||
return MergeCommandResult.failure(branchToMerge, targetBranch, result.getConflicts());
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
@@ -51,7 +51,7 @@ class GitMergeWithSquash extends GitMergeStrategy {
|
||||
if (result.getMergeStatus().isSuccessful()) {
|
||||
RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository()));
|
||||
push();
|
||||
return MergeCommandResult.success(getTargetRevision().name(), revCommit.name(), extractRevisionFromRevCommit(revCommit));
|
||||
return createSuccessResult(extractRevisionFromRevCommit(revCommit));
|
||||
} else {
|
||||
return analyseFailure(result);
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
Repository repository = createContext().open();
|
||||
assertThat(mergeCommandResult.isSuccess()).isTrue();
|
||||
assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo(mergeCommandResult.getNewHeadRevision());
|
||||
assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27");
|
||||
assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
|
||||
|
||||
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
|
||||
|
||||
@@ -51,7 +51,7 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
|
||||
const {
|
||||
isLoading: isUpdating,
|
||||
error: mutationError,
|
||||
mutate,
|
||||
mutateAsync,
|
||||
data: updateResponse,
|
||||
} = useMutation<Response, Error, MutationVariables<C>>(
|
||||
(vars: MutationVariables<C>) => apiClient.put(vars.link, vars.configuration, vars.contentType),
|
||||
@@ -67,7 +67,7 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
|
||||
const update = useCallback(
|
||||
(configuration: C) => {
|
||||
if (data && !isReadOnly) {
|
||||
mutate({
|
||||
return mutateAsync({
|
||||
configuration,
|
||||
contentType: data.contentType,
|
||||
link: (data.configuration._links.update as Link).href,
|
||||
@@ -76,7 +76,7 @@ export const useConfigLink = <C extends HalRepresentation>(link: string) => {
|
||||
},
|
||||
// eslint means we should add C to the dependency array, but C is only a type
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[mutate, data, isReadOnly]
|
||||
[mutateAsync, data, isReadOnly]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@scm-manager/ui-buttons",
|
||||
"version": "2.40.2-SNAPSHOT",
|
||||
"private": true,
|
||||
"private": false,
|
||||
"main": "build/index.js",
|
||||
"module": "build/index.mjs",
|
||||
"types": "build/index.d.ts",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "reac
|
||||
import classNames from "classnames";
|
||||
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||
|
||||
/** @Beta */
|
||||
export const ButtonVariants = {
|
||||
PRIMARY: "primary",
|
||||
SECONDARY: "secondary",
|
||||
@@ -57,6 +58,7 @@ type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
/**
|
||||
* Styled html button
|
||||
* @Beta
|
||||
*/
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||
@@ -75,6 +77,7 @@ type LinkButtonProps = BaseButtonProps & ReactRouterLinkProps;
|
||||
|
||||
/**
|
||||
* Styled react router link
|
||||
* @Beta
|
||||
*/
|
||||
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
||||
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||
@@ -93,6 +96,7 @@ type ExternalLinkButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchor
|
||||
|
||||
/**
|
||||
* Styled html anchor
|
||||
* @Beta
|
||||
*/
|
||||
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
|
||||
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||
|
||||
@@ -21,16 +21,16 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import React, { ComponentProps, FC } from "react";
|
||||
import { BackendError } from "@scm-manager/ui-api";
|
||||
import Notification from "./Notification";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
type Props = Omit<ComponentProps<typeof Notification>, "type" | "role"> & {
|
||||
error: BackendError;
|
||||
};
|
||||
|
||||
const BackendErrorNotification: FC<Props> = ({ error }) => {
|
||||
const BackendErrorNotification: FC<Props> = ({ error, ...props }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
|
||||
const renderErrorName = () => {
|
||||
@@ -141,7 +141,7 @@ const BackendErrorNotification: FC<Props> = ({ error }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Notification type="danger" role="alert">
|
||||
<Notification type="danger" role="alert" {...props}>
|
||||
<div className="content">
|
||||
<p className="subtitle">
|
||||
{t("error.subtitle")}
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import React, { ComponentProps, FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BackendError, ForbiddenError, UnauthorizedError, urls } from "@scm-manager/ui-api";
|
||||
import Notification from "./Notification";
|
||||
import BackendErrorNotification from "./BackendErrorNotification";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
type Props = ComponentProps<typeof BasicErrorMessage> & {
|
||||
error?: Error | null;
|
||||
};
|
||||
|
||||
@@ -40,31 +40,31 @@ const LoginLink: FC = () => {
|
||||
return <a href={urls.withContextPath(`/login?from=${from}`)}>{t("errorNotification.loginLink")}</a>;
|
||||
};
|
||||
|
||||
const BasicErrorMessage: FC = ({ children }) => {
|
||||
const BasicErrorMessage: FC<Omit<ComponentProps<typeof Notification>, "type" | "role">> = ({ children, ...props }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
|
||||
return (
|
||||
<Notification type="danger" role="alert">
|
||||
<Notification type="danger" role="alert" {...props}>
|
||||
<strong>{t("errorNotification.prefix")}:</strong> {children}
|
||||
</Notification>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorNotification: FC<Props> = ({ error }) => {
|
||||
const ErrorNotification: FC<Props> = ({ error, ...props }) => {
|
||||
const [t] = useTranslation("commons");
|
||||
if (error) {
|
||||
if (error instanceof BackendError) {
|
||||
return <BackendErrorNotification error={error} />;
|
||||
return <BackendErrorNotification error={error} {...props} />;
|
||||
} else if (error instanceof UnauthorizedError) {
|
||||
return (
|
||||
<BasicErrorMessage>
|
||||
<BasicErrorMessage {...props}>
|
||||
{t("errorNotification.timeout")} <LoginLink />
|
||||
</BasicErrorMessage>
|
||||
);
|
||||
} else if (error instanceof ForbiddenError) {
|
||||
return <BasicErrorMessage>{t("errorNotification.forbidden")}</BasicErrorMessage>;
|
||||
return <BasicErrorMessage {...props}>{t("errorNotification.forbidden")}</BasicErrorMessage>;
|
||||
} else {
|
||||
return <BasicErrorMessage>{error.message}</BasicErrorMessage>;
|
||||
return <BasicErrorMessage {...props}>{error.message}</BasicErrorMessage>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { binder, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import { NavLink } from "../navigation";
|
||||
import { Route } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Repository, Links, Link } from "@scm-manager/ui-types";
|
||||
import { Link, Links, Repository } from "@scm-manager/ui-types";
|
||||
import { urls } from "@scm-manager/ui-api";
|
||||
|
||||
type GlobalRouteProps = {
|
||||
@@ -44,8 +44,13 @@ type RepositoryNavProps = WithTranslation & { url: string };
|
||||
class ConfigurationBinder {
|
||||
i18nNamespace = "plugins";
|
||||
|
||||
navLink(to: string, labelI18nKey: string, t: any) {
|
||||
return <NavLink to={to} label={t(labelI18nKey)} />;
|
||||
navLink(
|
||||
to: string,
|
||||
labelI18nKey: string,
|
||||
t: any,
|
||||
options: Omit<ComponentProps<typeof NavLink>, "label" | "to"> = {}
|
||||
) {
|
||||
return <NavLink to={to} label={t(labelI18nKey)} {...options} />;
|
||||
}
|
||||
|
||||
route(path: string, Component: any) {
|
||||
@@ -86,6 +91,27 @@ class ConfigurationBinder {
|
||||
binder.bind("admin.route", ConfigRoute, configPredicate);
|
||||
}
|
||||
|
||||
bindAdmin(
|
||||
to: string,
|
||||
labelI18nKey: string,
|
||||
icon: string,
|
||||
linkName: string,
|
||||
Component: React.ComponentType<{ link: string }>
|
||||
) {
|
||||
const predicate = ({ links }: extensionPoints.AdminRoute["props"]) => links[linkName];
|
||||
|
||||
const AdminNavLink = withTranslation(this.i18nNamespace)(
|
||||
({ t, url }: WithTranslation & extensionPoints.AdminNavigation["props"]) =>
|
||||
this.navLink(url + to, labelI18nKey, t, { icon })
|
||||
);
|
||||
|
||||
const AdminRoute: extensionPoints.AdminRoute["type"] = ({ links, url }) =>
|
||||
this.route(url + to, <Component link={(links[linkName] as Link).href} />);
|
||||
|
||||
binder.bind<extensionPoints.AdminRoute>("admin.route", AdminRoute, predicate);
|
||||
binder.bind<extensionPoints.AdminNavigation>("admin.navigation", AdminNavLink, predicate);
|
||||
}
|
||||
|
||||
bindRepository(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
|
||||
// create predicate based on the link name of the current repository route
|
||||
// if the linkname is not available, the navigation link and the route are not bound to the extension points
|
||||
|
||||
@@ -660,3 +660,8 @@ export type FileViewActionBarOverflowMenu = ExtensionPointDefinition<
|
||||
>;
|
||||
|
||||
export type LoginForm = RenderableExtensionPointDefinition<"login.form">;
|
||||
|
||||
export type RepositoryDeleteButton = RenderableExtensionPointDefinition<
|
||||
"repository.deleteButton",
|
||||
{ repository: Repository }
|
||||
>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scm-manager/ui-forms",
|
||||
"private": true,
|
||||
"private": false,
|
||||
"version": "2.40.2-SNAPSHOT",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
@@ -40,7 +40,8 @@
|
||||
"react-query": "3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-buttons": "^2.40.2-SNAPSHOT"
|
||||
"@scm-manager/ui-buttons": "^2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-api": "^2.40.2-SNAPSHOT"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
"eslintConfig": {
|
||||
|
||||
54
scm-ui/ui-forms/src/ConfigurationForm.tsx
Normal file
54
scm-ui/ui-forms/src/ConfigurationForm.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import {useConfigLink} from "@scm-manager/ui-api";
|
||||
import {Loading} from "@scm-manager/ui-components";
|
||||
import React, {ComponentProps} from "react";
|
||||
import {HalRepresentation} from "@scm-manager/ui-types";
|
||||
import Form from "./Form";
|
||||
|
||||
type Props<T extends HalRepresentation> = Pick<ComponentProps<typeof Form<T, T>>, "translationPath" | "children"> & {
|
||||
link: string;
|
||||
};
|
||||
|
||||
/** @Beta */
|
||||
export function ConfigurationForm<T extends HalRepresentation>({link, translationPath, children}: Props<T>) {
|
||||
const {initialConfiguration, isReadOnly, update, isLoading} = useConfigLink<T>(link);
|
||||
|
||||
if (isLoading || !initialConfiguration) {
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form<T, T>
|
||||
onSubmit={update}
|
||||
translationPath={translationPath}
|
||||
defaultValues={initialConfiguration}
|
||||
readOnly={isReadOnly}
|
||||
>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfigurationForm;
|
||||
@@ -33,6 +33,7 @@ import ControlledInputField from "./input/ControlledInputField";
|
||||
import ControlledCheckboxField from "./checkbox/ControlledCheckboxField";
|
||||
import ControlledSecretConfirmationField from "./input/ControlledSecretConfirmationField";
|
||||
import { HalRepresentation } from "@scm-manager/ui-types";
|
||||
import ControlledSelectField from "./select/ControlledSelectField";
|
||||
|
||||
type RenderProps<T extends Record<string, unknown>> = Omit<
|
||||
UseFormReturn<T>,
|
||||
@@ -61,6 +62,7 @@ type Props<FormType extends Record<string, unknown>, DefaultValues extends FormT
|
||||
submitButtonTestId?: string;
|
||||
};
|
||||
|
||||
/** @Beta */
|
||||
function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType>({
|
||||
children,
|
||||
onSubmit,
|
||||
@@ -84,11 +86,11 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
setShowSuccessNotification(true);
|
||||
reset(defaultValues as never);
|
||||
}
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [isSubmitSuccessful]);
|
||||
|
||||
useEffect(() => reset(defaultValues as never), [defaultValues, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
setShowSuccessNotification(false);
|
||||
@@ -158,4 +160,5 @@ export default Object.assign(Form, {
|
||||
Input: ControlledInputField,
|
||||
Checkbox: ControlledCheckboxField,
|
||||
SecretConfirmation: ControlledSecretConfirmationField,
|
||||
Select: ControlledSelectField,
|
||||
});
|
||||
|
||||
@@ -23,4 +23,5 @@
|
||||
*/
|
||||
|
||||
export { default as Form } from "./Form";
|
||||
export { default as ConfigurationForm } from "./ConfigurationForm";
|
||||
export * from "./resourceHooks";
|
||||
|
||||
@@ -66,6 +66,7 @@ const createResource = <I, O = never>(link: string, contentType: string) => {
|
||||
|
||||
type CreateResourceOptions = MutatingResourceOptions;
|
||||
|
||||
/** @Beta */
|
||||
export const useCreateResource = <I, O>(
|
||||
link: string,
|
||||
[entityKey, collectionName]: QueryKeyPair,
|
||||
@@ -91,6 +92,7 @@ type UpdateResourceOptions = MutatingResourceOptions & {
|
||||
collectionName?: QueryKeyPair;
|
||||
};
|
||||
|
||||
/** @Beta */
|
||||
export const useUpdateResource = <T>(
|
||||
link: LinkOrHalLink,
|
||||
idFactory: (createdResource: T) => string,
|
||||
@@ -123,6 +125,7 @@ type DeleteResourceOptions = {
|
||||
collectionName?: QueryKeyPair;
|
||||
};
|
||||
|
||||
/** @Beta */
|
||||
export const useDeleteResource = <T extends HalRepresentation>(
|
||||
idFactory: (createdResource: T) => string,
|
||||
{ collectionName: [entityQueryKey, collectionName] = ["", ""] }: DeleteResourceOptions = {}
|
||||
|
||||
77
scm-ui/ui-forms/src/select/ControlledSelectField.tsx
Normal file
77
scm-ui/ui-forms/src/select/ControlledSelectField.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Controller, ControllerRenderProps, Path } from "react-hook-form";
|
||||
import classNames from "classnames";
|
||||
import { useScmFormContext } from "../ScmFormContext";
|
||||
import SelectField from "./SelectField";
|
||||
|
||||
type Props<T extends Record<string, unknown>> = Omit<
|
||||
ComponentProps<typeof SelectField>,
|
||||
"error" | "label" | "required" | keyof ControllerRenderProps
|
||||
> & {
|
||||
rules?: ComponentProps<typeof Controller>["rules"];
|
||||
name: Path<T>;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
function ControlledSelectField<T extends Record<string, unknown>>({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
rules,
|
||||
className,
|
||||
testId,
|
||||
defaultValue,
|
||||
readOnly,
|
||||
...props
|
||||
}: Props<T>) {
|
||||
const { control, t, readOnly: formReadonly } = useScmFormContext();
|
||||
const labelTranslation = label || t(`${name}.label`) || "";
|
||||
const helpTextTranslation = helpText || t(`${name}.helpText`);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
rules={rules}
|
||||
defaultValue={defaultValue as never}
|
||||
render={({ field, fieldState }) => (
|
||||
<SelectField
|
||||
className={classNames("column", className)}
|
||||
readOnly={readOnly ?? formReadonly}
|
||||
required={rules?.required as boolean}
|
||||
{...props}
|
||||
{...field}
|
||||
label={labelTranslation}
|
||||
helpText={helpTextTranslation}
|
||||
error={fieldState.error ? fieldState.error.message || t(`${name}.error.${fieldState.error.type}`) : undefined}
|
||||
testId={testId ?? `select-${name}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ControlledSelectField;
|
||||
43
scm-ui/ui-forms/src/select/Select.tsx
Normal file
43
scm-ui/ui-forms/src/select/Select.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { InputHTMLAttributes } from "react";
|
||||
import classNames from "classnames";
|
||||
import { createVariantClass, Variant } from "../variants";
|
||||
import { createAttributesForTesting } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
variant?: Variant;
|
||||
testId?: string;
|
||||
} & InputHTMLAttributes<HTMLSelectElement>;
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, Props>(({ variant, children, className, testId, ...props }, ref) => (
|
||||
<div className={classNames("select", { "is-multiple": props.multiple }, createVariantClass(variant), className)}>
|
||||
<select ref={ref} {...props} {...createAttributesForTesting(testId)}>
|
||||
{children}
|
||||
</select>
|
||||
</div>
|
||||
));
|
||||
|
||||
export default Select;
|
||||
59
scm-ui/ui-forms/src/select/SelectField.tsx
Normal file
59
scm-ui/ui-forms/src/select/SelectField.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import Field from "../base/Field";
|
||||
import Control from "../base/Control";
|
||||
import Label from "../base/label/Label";
|
||||
import FieldMessage from "../base/field-message/FieldMessage";
|
||||
import Help from "../base/help/Help";
|
||||
import Select from "./Select";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
helpText?: string;
|
||||
error?: string;
|
||||
} & React.ComponentProps<typeof Select>;
|
||||
|
||||
/**
|
||||
* @see https://bulma.io/documentation/form/select/
|
||||
*/
|
||||
const SelectField = React.forwardRef<HTMLSelectElement, Props>(
|
||||
({ label, helpText, error, className, ...props }, ref) => {
|
||||
const variant = error ? "danger" : undefined;
|
||||
return (
|
||||
<Field className={className}>
|
||||
<Label>
|
||||
{label}
|
||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||
</Label>
|
||||
<Control>
|
||||
<Select variant={variant} ref={ref} {...props}></Select>
|
||||
</Control>
|
||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default SelectField;
|
||||
@@ -8,6 +8,8 @@
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-components": "2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-extensions": "2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-forms": "2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "2.40.2-SNAPSHOT",
|
||||
"classnames": "^2.2.6",
|
||||
"query-string": "6.14.1",
|
||||
"react": "^17.0.1",
|
||||
|
||||
@@ -35,11 +35,11 @@ if (orgaIndex > 0) {
|
||||
name = name.substring(orgaIndex + 1);
|
||||
}
|
||||
|
||||
module.exports = function(mode) {
|
||||
module.exports = function (mode) {
|
||||
return {
|
||||
context: root,
|
||||
entry: {
|
||||
[name]: [path.resolve(__dirname, "webpack-public-path.js"), packageJSON.main || "src/main/js/index.js"]
|
||||
[name]: [path.resolve(__dirname, "webpack-public-path.js"), packageJSON.main || "src/main/js/index.js"],
|
||||
},
|
||||
mode,
|
||||
stats: "minimal",
|
||||
@@ -48,7 +48,7 @@ module.exports = function(mode) {
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: "empty",
|
||||
tls: "empty"
|
||||
tls: "empty",
|
||||
},
|
||||
externals: [
|
||||
"react",
|
||||
@@ -59,11 +59,13 @@ module.exports = function(mode) {
|
||||
"@scm-manager/ui-types",
|
||||
"@scm-manager/ui-extensions",
|
||||
"@scm-manager/ui-components",
|
||||
"@scm-manager/ui-forms",
|
||||
"@scm-manager/ui-buttons",
|
||||
"classnames",
|
||||
"query-string",
|
||||
"redux",
|
||||
"react-redux",
|
||||
/^@scm-manager\/scm-.*-plugin$/i
|
||||
/^@scm-manager\/scm-.*-plugin$/i,
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
@@ -73,29 +75,29 @@ module.exports = function(mode) {
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["@scm-manager/babel-preset"]
|
||||
}
|
||||
}
|
||||
presets: ["@scm-manager/babel-preset"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss|sass)$/i,
|
||||
use: ["style-loader", "css-loader", "sass-loader"]
|
||||
use: ["style-loader", "css-loader", "sass-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
|
||||
use: ["file-loader"]
|
||||
}
|
||||
]
|
||||
use: ["file-loader"],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"]
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss", ".json"],
|
||||
},
|
||||
output: {
|
||||
path: path.join(root, "target", `${name}-${packageJSON.version}`, "webapp", "assets"),
|
||||
filename: "[name].bundle.js",
|
||||
chunkFilename: `${name}.[name].chunk.js`,
|
||||
library: name,
|
||||
libraryTarget: "amd"
|
||||
}
|
||||
libraryTarget: "amd",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@scm-manager/ui-shortcuts": "2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-legacy": "2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-forms": "2.40.2-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "2.40.2-SNAPSHOT",
|
||||
"classnames": "^2.2.5",
|
||||
"history": "^4.10.1",
|
||||
"i18next": "21",
|
||||
|
||||
@@ -29,6 +29,7 @@ import RenameRepository from "./RenameRepository";
|
||||
import DeleteRepo from "./DeleteRepo";
|
||||
import ArchiveRepo from "./ArchiveRepo";
|
||||
import UnarchiveRepo from "./UnarchiveRepo";
|
||||
import { ExtensionPoint, extensionPoints, useBinder } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -36,12 +37,18 @@ type Props = {
|
||||
|
||||
const RepositoryDangerZone: FC<Props> = ({ repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const binder = useBinder();
|
||||
|
||||
const dangerZone = [];
|
||||
if (repository?._links?.rename || repository?._links?.renameWithNamespace) {
|
||||
dangerZone.push(<RenameRepository repository={repository} />);
|
||||
}
|
||||
if (repository?._links?.delete) {
|
||||
|
||||
if (binder.hasExtension<extensionPoints.RepositoryDeleteButton>("repository.deleteButton", { repository })) {
|
||||
dangerZone.push(
|
||||
<ExtensionPoint<extensionPoints.RepositoryDeleteButton> name="repository.deleteButton" props={{ repository }} />
|
||||
);
|
||||
} else if (repository?._links?.delete) {
|
||||
dangerZone.push(<DeleteRepo repository={repository} />);
|
||||
}
|
||||
if (repository?._links?.archive) {
|
||||
|
||||
@@ -27,7 +27,7 @@ package sonia.scm.importexport;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.FullRepositoryExporter;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.api.ExportFailedException;
|
||||
@@ -36,6 +36,7 @@ import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.work.WorkdirProvider;
|
||||
import sonia.scm.util.Archives;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.BufferedOutputStream;
|
||||
@@ -46,8 +47,10 @@ import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
public class FullScmRepositoryExporter {
|
||||
|
||||
public class FullScmRepositoryExporter implements FullRepositoryExporter {
|
||||
|
||||
static final String SCM_ENVIRONMENT_FILE_NAME = "scm-environment.xml";
|
||||
static final String METADATA_FILE_NAME = "metadata.xml";
|
||||
@@ -61,6 +64,8 @@ public class FullScmRepositoryExporter {
|
||||
private final RepositoryImportExportEncryption repositoryImportExportEncryption;
|
||||
private final ExportNotificationHandler notificationHandler;
|
||||
|
||||
private final AdministrationContext administrationContext;
|
||||
|
||||
@Inject
|
||||
public FullScmRepositoryExporter(EnvironmentInformationXmlGenerator environmentGenerator,
|
||||
RepositoryMetadataXmlGenerator metadataGenerator,
|
||||
@@ -68,7 +73,7 @@ public class FullScmRepositoryExporter {
|
||||
TarArchiveRepositoryStoreExporter storeExporter,
|
||||
WorkdirProvider workdirProvider,
|
||||
RepositoryExportingCheck repositoryExportingCheck,
|
||||
RepositoryImportExportEncryption repositoryImportExportEncryption, ExportNotificationHandler notificationHandler) {
|
||||
RepositoryImportExportEncryption repositoryImportExportEncryption, ExportNotificationHandler notificationHandler, AdministrationContext administrationContext) {
|
||||
this.environmentGenerator = environmentGenerator;
|
||||
this.metadataGenerator = metadataGenerator;
|
||||
this.serviceFactory = serviceFactory;
|
||||
@@ -77,6 +82,7 @@ public class FullScmRepositoryExporter {
|
||||
this.repositoryExportingCheck = repositoryExportingCheck;
|
||||
this.repositoryImportExportEncryption = repositoryImportExportEncryption;
|
||||
this.notificationHandler = notificationHandler;
|
||||
this.administrationContext = administrationContext;
|
||||
}
|
||||
|
||||
public void export(Repository repository, OutputStream outputStream, String password) {
|
||||
@@ -99,27 +105,33 @@ public class FullScmRepositoryExporter {
|
||||
GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(cos);
|
||||
TarArchiveOutputStream taos = Archives.createTarOutputStream(gzos);
|
||||
) {
|
||||
writeEnvironmentData(taos);
|
||||
writeEnvironmentData(repository, taos);
|
||||
writeMetadata(repository, taos);
|
||||
writeStoreData(repository, taos);
|
||||
writeRepository(service, taos);
|
||||
taos.finish();
|
||||
} catch (IOException e) {
|
||||
throw new ExportFailedException(
|
||||
ContextEntry.ContextBuilder.entity(repository).build(),
|
||||
entity(repository).build(),
|
||||
"Could not export repository with metadata",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEnvironmentData(TarArchiveOutputStream taos) throws IOException {
|
||||
byte[] envBytes = environmentGenerator.generate();
|
||||
TarArchiveEntry entry = new TarArchiveEntry(SCM_ENVIRONMENT_FILE_NAME);
|
||||
entry.setSize(envBytes.length);
|
||||
taos.putArchiveEntry(entry);
|
||||
taos.write(envBytes);
|
||||
taos.closeArchiveEntry();
|
||||
private void writeEnvironmentData(Repository repository, TarArchiveOutputStream taos) {
|
||||
administrationContext.runAsAdmin(() -> {
|
||||
byte[] envBytes = environmentGenerator.generate();
|
||||
TarArchiveEntry entry = new TarArchiveEntry(SCM_ENVIRONMENT_FILE_NAME);
|
||||
entry.setSize(envBytes.length);
|
||||
try {
|
||||
taos.putArchiveEntry(entry);
|
||||
taos.write(envBytes);
|
||||
taos.closeArchiveEntry();
|
||||
} catch (IOException e) {
|
||||
throw new ExportFailedException(entity(repository).build(), "Failed to collect instance environment for repository export", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void writeMetadata(Repository repository, TarArchiveOutputStream taos) throws IOException {
|
||||
|
||||
@@ -32,10 +32,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryImportEvent;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.*;
|
||||
import sonia.scm.repository.api.ImportFailedException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -48,7 +45,7 @@ import static sonia.scm.ContextEntry.ContextBuilder.noContext;
|
||||
import static sonia.scm.importexport.RepositoryImportLogger.ImportType.FULL;
|
||||
import static sonia.scm.util.Archives.createTarInputStream;
|
||||
|
||||
public class FullScmRepositoryImporter {
|
||||
public class FullScmRepositoryImporter implements FullRepositoryImporter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FullScmRepositoryImporter.class);
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ import sonia.scm.group.GroupDisplayManager;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.group.GroupManagerProvider;
|
||||
import sonia.scm.group.xml.XmlGroupDAO;
|
||||
import sonia.scm.importexport.FullScmRepositoryExporter;
|
||||
import sonia.scm.importexport.FullScmRepositoryImporter;
|
||||
import sonia.scm.initialization.DefaultInitializationFinisher;
|
||||
import sonia.scm.initialization.InitializationCookieIssuer;
|
||||
import sonia.scm.initialization.InitializationFinisher;
|
||||
@@ -76,24 +78,7 @@ import sonia.scm.notifications.NotificationSender;
|
||||
import sonia.scm.plugin.DefaultPluginManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.repository.DefaultHealthCheckService;
|
||||
import sonia.scm.repository.DefaultNamespaceManager;
|
||||
import sonia.scm.repository.DefaultRepositoryManager;
|
||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||
import sonia.scm.repository.DefaultRepositoryRoleManager;
|
||||
import sonia.scm.repository.HealthCheckContextListener;
|
||||
import sonia.scm.repository.HealthCheckService;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceStrategyProvider;
|
||||
import sonia.scm.repository.PermissionProvider;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryManagerProvider;
|
||||
import sonia.scm.repository.RepositoryProvider;
|
||||
import sonia.scm.repository.RepositoryRoleDAO;
|
||||
import sonia.scm.repository.RepositoryRoleManager;
|
||||
import sonia.scm.repository.*;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.spi.HookEventFacade;
|
||||
@@ -297,6 +282,9 @@ class ScmServletModule extends ServletModule {
|
||||
bind(CentralWorkQueue.class, DefaultCentralWorkQueue.class);
|
||||
|
||||
bind(ContentTypeResolver.class).to(DefaultContentTypeResolver.class);
|
||||
|
||||
bind(FullRepositoryImporter.class).to(FullScmRepositoryImporter.class);
|
||||
bind(FullRepositoryExporter.class).to(FullScmRepositoryExporter.class);
|
||||
}
|
||||
|
||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.store.ConfigurationStore;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.web.JsonMockHttpRequest;
|
||||
import sonia.scm.web.JsonMockHttpResponse;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||
@SubjectAware(value = "trillian")
|
||||
class ConfigurationAdapterBaseTest {
|
||||
|
||||
@Mock
|
||||
private ConfigurationStore<TestConfiguration> configStore;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private ConfigurationStoreFactory configurationStoreFactory;
|
||||
|
||||
@Mock
|
||||
private Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
private RestDispatcher dispatcher;
|
||||
private final JsonMockHttpResponse response = new JsonMockHttpResponse();
|
||||
|
||||
@BeforeEach
|
||||
void initResource() {
|
||||
when(configurationStoreFactory.withType(TestConfiguration.class).withName("test-config").build()).thenReturn(configStore);
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("v2/test"));
|
||||
lenient().when(scmPathInfoStore.get()).thenReturn(pathInfoStore);
|
||||
TestConfigurationAdapter resource = new TestConfigurationAdapter(configurationStoreFactory, scmPathInfoStore);
|
||||
|
||||
dispatcher = new RestDispatcher();
|
||||
dispatcher.addSingletonResource(resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowBadRequestErrorWhenUpdatingWithNullPayload() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.put("/v2/test")
|
||||
.contentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionWithoutPermissionOnRead() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/test");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionWithoutPermissionOnUpdate() throws URISyntaxException {
|
||||
JsonMockHttpRequest request = JsonMockHttpRequest.put("/v2/test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.json("{\"number\": 3, \"text\" : \"https://scm-manager.org/\"}");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SubjectAware(permissions = "configuration:read,write:testConfig")
|
||||
class WithPermission {
|
||||
|
||||
@Test
|
||||
void shouldGetDefaultDao() throws URISyntaxException {
|
||||
when(configStore.getOptional()).thenReturn(Optional.empty());
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/test");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsJson().get("number").asInt()).isEqualTo(4);
|
||||
assertThat(response.getContentAsJson().get("text").textValue()).isEqualTo("myTest");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetStoredDao() throws URISyntaxException {
|
||||
when(configStore.getOptional()).thenReturn(Optional.of(new TestConfiguration(3, "secret")));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/test");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsJson().get("number").asInt()).isEqualTo(3);
|
||||
assertThat(response.getContentAsJson().get("text").textValue()).isEqualTo("secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowBadRequestErrorWhenUpdatingWithInvalidPayload() throws URISyntaxException {
|
||||
JsonMockHttpRequest request = JsonMockHttpRequest.put("/v2/test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.json("{\"number\": 42, \"text\" : \"https://scm-manager.org/\"}");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
static class TestConfiguration {
|
||||
private int number = 4;
|
||||
private String text = "myTest";
|
||||
}
|
||||
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
static class TestConfigurationDto extends HalRepresentation {
|
||||
|
||||
@Min(2)
|
||||
@Max(6)
|
||||
private int number;
|
||||
|
||||
@NotBlank
|
||||
private String text;
|
||||
|
||||
public TestConfigurationDto(int number, String text) {
|
||||
super(Links.emptyLinks(), Embedded.emptyEmbedded());
|
||||
this.number = number;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public static TestConfigurationDto from(TestConfiguration configuration, Links links) {
|
||||
return new TestConfigurationDto(configuration.number, configuration.text);
|
||||
}
|
||||
|
||||
public TestConfiguration toEntity() {
|
||||
return new TestConfiguration(number, text);
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/v2/test")
|
||||
private static class TestConfigurationAdapter extends ConfigurationAdapterBase<TestConfiguration, TestConfigurationDto> {
|
||||
|
||||
@Inject
|
||||
public TestConfigurationAdapter(ConfigurationStoreFactory configurationStoreFactory, Provider<ScmPathInfoStore> scmPathInfoStoreProvider) {
|
||||
super(configurationStoreFactory, scmPathInfoStoreProvider, TestConfiguration.class, TestConfigurationDto.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getName() {
|
||||
return "testConfig";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import sonia.scm.repository.api.BundleCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.work.WorkdirProvider;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
import sonia.scm.web.security.PrivilegedAction;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -79,6 +81,8 @@ class FullScmRepositoryExporterTest {
|
||||
@Mock
|
||||
private RepositoryExportingCheck repositoryExportingCheck;
|
||||
@Mock
|
||||
private AdministrationContext administrationContext;
|
||||
@Mock
|
||||
private RepositoryImportExportEncryption repositoryImportExportEncryption;
|
||||
|
||||
@InjectMocks
|
||||
@@ -89,14 +93,13 @@ class FullScmRepositoryExporterTest {
|
||||
@BeforeEach
|
||||
void initRepoService() throws IOException {
|
||||
when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService);
|
||||
when(environmentGenerator.generate()).thenReturn(new byte[0]);
|
||||
when(metadataGenerator.generate(REPOSITORY)).thenReturn(new byte[0]);
|
||||
when(repositoryExportingCheck.withExportingLock(any(), any())).thenAnswer(invocation -> invocation.getArgument(1, Supplier.class).get());
|
||||
when(repositoryImportExportEncryption.optionallyEncrypt(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExportEverythingAsTarArchive(@TempDir Path temp) throws IOException {
|
||||
void shouldExportEverythingAsTarArchive(@TempDir Path temp) {
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(repositoryService.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
when(repositoryService.getRepository()).thenReturn(REPOSITORY);
|
||||
@@ -105,7 +108,7 @@ class FullScmRepositoryExporterTest {
|
||||
exporter.export(REPOSITORY, baos, "");
|
||||
|
||||
verify(storeExporter, times(1)).export(eq(REPOSITORY), any(OutputStream.class));
|
||||
verify(environmentGenerator, times(1)).generate();
|
||||
verify(administrationContext, times(1)).runAsAdmin(any(PrivilegedAction.class));
|
||||
verify(metadataGenerator, times(1)).generate(REPOSITORY);
|
||||
verify(bundleCommandBuilder, times(1)).bundle(any(OutputStream.class));
|
||||
verify(repositoryExportingCheck).withExportingLock(eq(REPOSITORY), any());
|
||||
|
||||
Reference in New Issue
Block a user