Integrate import repository function into ui

This commit is contained in:
Eduard Heimbuch
2020-11-25 11:23:43 +01:00
parent ff2b4d8acd
commit 10ed51733c
7 changed files with 251 additions and 205 deletions

View File

@@ -39,6 +39,12 @@ export type RepositoryCreation = Repository & {
contextEntries: { [key: string]: any };
};
export type RepositoryImport = Repository & {
importUrl?: string;
username?: string;
password?: string;
};
export type Namespace = {
namespace: string;
_links: Links;

View File

@@ -37,7 +37,7 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import CreateUser from "../users/containers/CreateUser";
import SingleUser from "../users/containers/SingleUser";
import RepositoryRoot from "../repos/containers/RepositoryRoot";
import Create from "../repos/containers/Create";
import AddRepository from "../repos/containers/AddRepository";
import Groups from "../groups/containers/Groups";
import SingleGroup from "../groups/containers/SingleGroup";
@@ -77,8 +77,8 @@ class Main extends React.Component<Props> {
<Route path="/logout" component={Logout} />
<Redirect exact strict from="/repos" to="/repos/" />
<ProtectedRoute exact path="/repos/" component={Overview} authenticated={authenticated} />
<ProtectedRoute exact path="/repos/create" component={Create} authenticated={authenticated} />
<ProtectedRoute exact path="/repos/import" component={Create} authenticated={authenticated} />
<ProtectedRoute exact path="/repos/create" component={AddRepository} authenticated={authenticated} />
<ProtectedRoute exact path="/repos/import" component={AddRepository} authenticated={authenticated} />
<ProtectedRoute exact path="/repos/:namespace" component={Overview} authenticated={authenticated} />
<ProtectedRoute exact path="/repos/:namespace/:page" component={Overview} authenticated={authenticated} />
<ProtectedRoute path="/repo/:namespace/:name" component={RepositoryRoot} authenticated={authenticated} />

View File

@@ -25,7 +25,7 @@ import React, { FC, useEffect, useState } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Repository, RepositoryType } from "@scm-manager/ui-types";
import { Repository, RepositoryType, RepositoryImport } from "@scm-manager/ui-types";
import { Checkbox, InputField, Level, Select, SubmitButton, Textarea } from "@scm-manager/ui-components";
import * as validator from "./repositoryValidation";
import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos";
@@ -56,13 +56,13 @@ const Columns = styled.div`
type Props = {
createRepository?: (repo: RepositoryCreation, shouldInit: boolean) => void;
modifyRepository?: (repo: RepositoryCreation) => void;
importRepository?: (repo: RepositoryCreation) => void;
importRepository?: (repo: RepositoryImport) => void;
modifyRepository?: (repo: Repository) => void;
repository?: Repository;
repositoryTypes?: RepositoryType[];
namespaceStrategy?: string;
loading?: boolean;
indexResources: any;
indexResources?: any;
};
type RepositoryCreation = Repository & {
@@ -118,19 +118,14 @@ const RepositoryForm: FC<Props> = ({
const submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const submitForm = evaluateSubmit();
if (isValid() && submitForm) {
submitForm(repo, initRepository);
if (isValid()) {
if (importRepository && isImportPage()) {
importRepository({ ...repo, url: importUrl, username, password });
} else if (createRepository && isCreatePage()) {
createRepository(repo, initRepository);
} else if (modifyRepository) {
modifyRepository(repo);
}
};
const evaluateSubmit = () => {
if (isImportPage()) {
return importRepository;
} else if (isCreatePage()) {
return createRepository;
} else {
return modifyRepository;
}
};
@@ -167,13 +162,8 @@ const RepositoryForm: FC<Props> = ({
return "";
};
const isImportPage = () => {
return resolveLocation() === "import";
};
const isCreatePage = () => {
return resolveLocation() === "create";
};
const isImportPage = () => resolveLocation() === "import";
const isCreatePage = () => resolveLocation() === "create";
const createSelectOptions = (repositoryTypes?: RepositoryType[]) => {
if (repositoryTypes) {
@@ -311,7 +301,7 @@ const RepositoryForm: FC<Props> = ({
if (!repo.name) {
const match = url.match(/([^\/]+)\.git/i);
if (match && match[1]) {
setRepo({ ...repo, name: match[1] });
handleNameChange(match[1]);
}
}
setImportUrl(url);
@@ -319,9 +309,8 @@ const RepositoryForm: FC<Props> = ({
const disabled = !isModifiable() && isEditMode();
const getSubmitButtonTranslationKey = () => {
return isImportPage() ? "repositoryForm.submitImport" : "repositoryForm.submitCreate";
};
const getSubmitButtonTranslationKey = () =>
isImportPage() ? "repositoryForm.submitImport" : "repositoryForm.submitCreate";
const submitButton = disabled ? null : (
<Level

View File

@@ -25,7 +25,13 @@ import React from "react";
import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { NamespaceStrategies, Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types";
import {
NamespaceStrategies,
Repository,
RepositoryCreation,
RepositoryImport,
RepositoryType
} from "@scm-manager/ui-types";
import { Page } from "@scm-manager/ui-components";
import {
fetchRepositoryTypesIfNeeded,
@@ -34,7 +40,13 @@ import {
isFetchRepositoryTypesPending
} from "../modules/repositoryTypes";
import RepositoryForm from "../components/form";
import { createRepo, createRepoReset, getCreateRepoFailure, isCreateRepoPending } from "../modules/repos";
import {
createRepo,
createRepoReset,
getCreateRepoFailure,
importRepoFromUrl,
isCreateRepoPending
} from "../modules/repos";
import { getRepositoriesLink } from "../../modules/indexResource";
import {
fetchNamespaceStrategiesIfNeeded,
@@ -61,13 +73,14 @@ type Props = WithTranslation & {
initRepository: boolean,
callback: (repo: Repository) => void
) => void;
importRepoFromUrl: (link: string, repository: RepositoryImport, callback: (repo: Repository) => void) => void;
resetForm: () => void;
// context props
history: History;
};
class Create extends React.Component<Props> {
class AddRepository extends React.Component<Props> {
componentDidMount() {
this.props.resetForm();
this.props.fetchRepositoryTypesIfNeeded();
@@ -87,18 +100,15 @@ class Create extends React.Component<Props> {
repositoryTypes,
namespaceStrategies,
createRepo,
importRepoFromUrl,
error,
indexResources
indexResources,
repoLink,
t
} = this.props;
const { t, repoLink } = this.props;
return (
<Page
title={t("create.title")}
loading={pageLoading}
error={error}
showContentOnError={true}
>
<Page title={t("create.title")} loading={pageLoading} error={error} showContentOnError={true}>
<RepositoryForm
repositoryTypes={repositoryTypes}
loading={createLoading}
@@ -106,6 +116,9 @@ class Create extends React.Component<Props> {
createRepository={(repo, initRepository) => {
createRepo(repoLink, repo, initRepository, (repo: Repository) => this.repoCreated(repo));
}}
importRepository={repo => {
importRepoFromUrl(repoLink, repo, (repo: Repository) => this.repoCreated(repo));
}}
indexResources={indexResources}
/>
</Page>
@@ -145,10 +158,13 @@ const mapDispatchToProps = (dispatch: any) => {
createRepo: (link: string, repository: RepositoryCreation, initRepository: boolean, callback: () => void) => {
dispatch(createRepo(link, repository, initRepository, callback));
},
importRepoFromUrl: (link: string, repository: RepositoryImport, callback: () => void) => {
dispatch(importRepoFromUrl(link, repository, callback));
},
resetForm: () => {
dispatch(createRepoReset());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("repos")(Create));
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("repos")(AddRepository));

View File

@@ -75,7 +75,7 @@ class EditRepo extends React.Component<Props> {
<RepositoryForm
repository={this.props.repository}
loading={loading}
createRepository={repo => {
modifyRepository={repo => {
this.props.modifyRepo(repo, this.repoModified);
}}
/>

View File

@@ -25,11 +25,14 @@
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../modules/types";
import {
Action, Namespace,
Action,
Namespace,
NamespaceCollection,
Repository,
RepositoryCollection,
RepositoryCreation
RepositoryCreation,
RepositoryImport,
Link
} from "@scm-manager/ui-types";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
@@ -55,6 +58,12 @@ export const CREATE_REPO_SUCCESS = `${CREATE_REPO}_${types.SUCCESS_SUFFIX}`;
export const CREATE_REPO_FAILURE = `${CREATE_REPO}_${types.FAILURE_SUFFIX}`;
export const CREATE_REPO_RESET = `${CREATE_REPO}_${types.RESET_SUFFIX}`;
export const IMPORT_REPO = "scm/repos/IMPORT_REPO";
export const IMPORT_REPO_PENDING = `${IMPORT_REPO}_${types.PENDING_SUFFIX}`;
export const IMPORT_REPO_SUCCESS = `${IMPORT_REPO}_${types.SUCCESS_SUFFIX}`;
export const IMPORT_REPO_FAILURE = `${IMPORT_REPO}_${types.FAILURE_SUFFIX}`;
export const IMPORT_REPO_RESET = `${IMPORT_REPO}_${types.RESET_SUFFIX}`;
export const MODIFY_REPO = "scm/repos/MODIFY_REPO";
export const MODIFY_REPO_PENDING = `${MODIFY_REPO}_${types.PENDING_SUFFIX}`;
export const MODIFY_REPO_SUCCESS = `${MODIFY_REPO}_${types.SUCCESS_SUFFIX}`;
@@ -178,7 +187,7 @@ export function fetchNamespacesFailure(err: Error): Action {
// fetch repo
export function fetchRepoByLink(repo: Repository) {
return fetchRepo(repo._links.self.href, repo.namespace, repo.name);
return fetchRepo((repo._links.self as Link).href, repo.namespace, repo.name);
}
export function fetchRepoByName(link: string, namespace: string, name: string) {
@@ -232,6 +241,50 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error):
};
}
// import repo
export function importRepoFromUrl(link: string, repository: RepositoryImport, callback?: (repo: Repository) => void) {
const importLink = link + `import/${repository.type}/url`;
return function(dispatch: any) {
dispatch(importRepoPending());
return apiClient
.post(importLink, repository, CONTENT_TYPE)
.then(response => {
const location = response.headers.get("Location");
dispatch(importRepoSuccess());
return apiClient.get(location);
})
.then(response => response.json())
.then(response => {
if (callback) {
callback(response);
}
})
.catch(err => {
dispatch(importRepoFailure(err));
});
};
}
export function importRepoPending(): Action {
return {
type: CREATE_REPO_PENDING
};
}
export function importRepoSuccess(): Action {
return {
type: CREATE_REPO_SUCCESS
};
}
export function importRepoFailure(err: Error): Action {
return {
type: CREATE_REPO_FAILURE,
payload: err
};
}
// create repo
export function createRepo(
@@ -294,7 +347,7 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
dispatch(modifyRepoPending(repository));
return apiClient
.put(repository._links.update.href, repository, CONTENT_TYPE)
.put((repository._links.update as Link).href, repository, CONTENT_TYPE)
.then(() => {
dispatch(modifyRepoSuccess(repository));
if (callback) {
@@ -353,7 +406,7 @@ export function deleteRepo(repository: Repository, callback?: () => void) {
return function(dispatch: any) {
dispatch(deleteRepoPending(repository));
return apiClient
.delete(repository._links.delete.href)
.delete((repository._links.delete as Link).href)
.then(() => {
dispatch(deleteRepoSuccess(repository));
if (callback) {

View File

@@ -25,17 +25,18 @@
package sonia.scm.api.v2.resources;
import com.google.common.base.Strings;
import com.google.common.io.Files;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.ToString;
import lombok.Value;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.FeatureNotSupportedException;
import sonia.scm.NotFoundException;
import sonia.scm.Type;
import sonia.scm.repository.InternalRepositoryException;
@@ -47,7 +48,6 @@ import sonia.scm.repository.RepositoryType;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.util.IOUtil;
import sonia.scm.web.VndMediaType;
import javax.ws.rs.Consumes;
@@ -56,19 +56,10 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
@@ -80,12 +71,14 @@ public class RepositoryImportResource {
private final RepositoryManager manager;
private final RepositoryServiceFactory serviceFactory;
private final ResourceLinks resourceLinks;
@Inject
public RepositoryImportResource(RepositoryManager manager,
RepositoryServiceFactory serviceFactory) {
RepositoryServiceFactory serviceFactory, ResourceLinks resourceLinks) {
this.manager = manager;
this.serviceFactory = serviceFactory;
this.resourceLinks = resourceLinks;
}
// /**
@@ -171,7 +164,7 @@ public class RepositoryImportResource {
*/
@POST
@Path("{type}/url")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Consumes(VndMediaType.REPOSITORY)
@Operation(summary = "Import repository from url", description = "Imports the repository for the given url.", tags = "Repository")
@ApiResponse(
responseCode = "201",
@@ -198,7 +191,7 @@ public class RepositoryImportResource {
)
)
public Response importFromUrl(@Context UriInfo uriInfo,
@PathParam("type") String type, UrlImportRequest request) {
@PathParam("type") String type, RepositoryImportDto request) {
RepositoryPermissions.create().check();
checkNotNull(request, "request is required");
checkArgument(!Strings.isNullOrEmpty(request.getName()),
@@ -220,17 +213,7 @@ public class RepositoryImportResource {
handleImportFailure(ex, repository);
}
return Response.created(createRepositoryLocation(uriInfo, repository)).build();
}
private URI createRepositoryLocation(UriInfo uriInfo, Repository repository) {
return URI.create(
String.format(
"%s/repos/%s",
uriInfo.getBaseUri().toString().replace("/api/", "/"),
repository.getNamespaceAndName()
)
);
return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build();
}
// /**
@@ -408,81 +391,81 @@ public class RepositoryImportResource {
return repository;
}
/**
* Start bundle import.
*
* @param type repository type
* @param name name of the repository
* @param inputStream bundle stream
* @param compressed true if the bundle is gzip compressed
* @return imported repository
*/
private Repository doImportFromBundle(String type, String namespace, String name,
InputStream inputStream, boolean compressed) {
RepositoryPermissions.create().check();
checkArgument(!Strings.isNullOrEmpty(name),
"request does not contain name of the repository");
checkNotNull(inputStream, "bundle inputStream is required");
Repository repository;
try {
Type t = type(type);
checkSupport(t, Command.UNBUNDLE, "bundle");
repository = create(namespace, name, type);
importFromBundle(repository, inputStream, compressed);
} catch (IOException ex) {
logger.warn("could not create temporary file", ex);
throw new WebApplicationException(ex);
}
return repository;
}
private void importFromBundle(Repository repository, InputStream inputStream, boolean compressed) throws IOException {
File file = File.createTempFile("scm-import-", ".bundle");
try (RepositoryService service = serviceFactory.create(repository)) {
long length = Files.asByteSink(file).writeFrom(inputStream);
logger.info("copied {} bytes to temp, start bundle import", length);
service.getUnbundleCommand().setCompressed(compressed).unbundle(file);
} catch (InternalRepositoryException ex) {
handleImportFailure(ex, repository);
} finally {
IOUtil.delete(file);
}
}
private List<Type> findImportableTypes() {
List<Type> types = new ArrayList<>();
Collection<Type> handlerTypes = manager.getTypes();
for (Type t : handlerTypes) {
RepositoryHandler handler = manager.getHandler(t.getName());
if (handler != null) {
try {
if (handler.getImportHandler() != null) {
types.add(t);
}
} catch (FeatureNotSupportedException ex) {
if (logger.isTraceEnabled()) {
logger.trace("import handler is not supported", ex);
} else if (logger.isInfoEnabled()) {
logger.info("{} handler does not support import of repositories",
t.getName());
}
}
} else if (logger.isWarnEnabled()) {
logger.warn("could not find handler for type {}", t.getName());
}
}
return types;
}
// /**
// * Start bundle import.
// *
// * @param type repository type
// * @param name name of the repository
// * @param inputStream bundle stream
// * @param compressed true if the bundle is gzip compressed
// * @return imported repository
// */
// private Repository doImportFromBundle(String type, String namespace, String name,
// InputStream inputStream, boolean compressed) {
// RepositoryPermissions.create().check();
//
// checkArgument(!Strings.isNullOrEmpty(name),
// "request does not contain name of the repository");
// checkNotNull(inputStream, "bundle inputStream is required");
//
// Repository repository;
//
// try {
// Type t = type(type);
// checkSupport(t, Command.UNBUNDLE, "bundle");
// repository = create(namespace, name, type);
// importFromBundle(repository, inputStream, compressed);
// } catch (IOException ex) {
// logger.warn("could not create temporary file", ex);
//
// throw new WebApplicationException(ex);
// }
//
// return repository;
// }
//
// private void importFromBundle(Repository repository, InputStream inputStream, boolean compressed) throws IOException {
// File file = File.createTempFile("scm-import-", ".bundle");
//
// try (RepositoryService service = serviceFactory.create(repository)) {
// long length = Files.asByteSink(file).writeFrom(inputStream);
//
// logger.info("copied {} bytes to temp, start bundle import", length);
// service.getUnbundleCommand().setCompressed(compressed).unbundle(file);
// } catch (InternalRepositoryException ex) {
// handleImportFailure(ex, repository);
// } finally {
// IOUtil.delete(file);
// }
// }
//
// private List<Type> findImportableTypes() {
// List<Type> types = new ArrayList<>();
// Collection<Type> handlerTypes = manager.getTypes();
//
// for (Type t : handlerTypes) {
// RepositoryHandler handler = manager.getHandler(t.getName());
//
// if (handler != null) {
// try {
// if (handler.getImportHandler() != null) {
// types.add(t);
// }
// } catch (FeatureNotSupportedException ex) {
// if (logger.isTraceEnabled()) {
// logger.trace("import handler is not supported", ex);
// } else if (logger.isInfoEnabled()) {
// logger.info("{} handler does not support import of repositories",
// t.getName());
// }
// }
// } else if (logger.isWarnEnabled()) {
// logger.warn("could not find handler for type {}", t.getName());
// }
// }
//
// return types;
// }
/**
* Handle creation failures.
@@ -518,49 +501,49 @@ public class RepositoryImportResource {
Response.Status.INTERNAL_SERVER_ERROR);
}
/**
* Import repositories from a specific type.
*
* @param repositories repository list
* @param type type of repository
*/
private void importFromDirectory(List<Repository> repositories, String type) {
RepositoryHandler handler = manager.getHandler(type);
if (handler != null) {
logger.info("start directory import for repository type {}", type);
try {
List<String> repositoryNames =
handler.getImportHandler().importRepositories(manager);
if (repositoryNames != null) {
for (String repositoryName : repositoryNames) {
// TODO #8783
/*Repository repository = null; //manager.get(type, repositoryName);
if (repository != null)
{
repositories.add(repository);
}
else if (logger.isWarnEnabled())
{
logger.warn("could not find imported repository {}",
repositoryName);
}*/
}
}
} catch (FeatureNotSupportedException ex) {
throw new WebApplicationException(ex, Response.Status.BAD_REQUEST);
} catch (IOException ex) {
throw new WebApplicationException(ex);
} catch (InternalRepositoryException ex) {
throw new WebApplicationException(ex);
}
} else {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
}
// /**
// * Import repositories from a specific type.
// *
// * @param repositories repository list
// * @param type type of repository
// */
// private void importFromDirectory(List<Repository> repositories, String type) {
// RepositoryHandler handler = manager.getHandler(type);
//
// if (handler != null) {
// logger.info("start directory import for repository type {}", type);
//
// try {
// List<String> repositoryNames =
// handler.getImportHandler().importRepositories(manager);
//
// if (repositoryNames != null) {
// for (String repositoryName : repositoryNames) {
// // TODO #8783
// /*Repository repository = null; //manager.get(type, repositoryName);
//
// if (repository != null)
// {
// repositories.add(repository);
// }
// else if (logger.isWarnEnabled())
// {
// logger.warn("could not find imported repository {}",
// repositoryName);
// }*/
// }
// }
// } catch (FeatureNotSupportedException ex) {
// throw new WebApplicationException(ex, Response.Status.BAD_REQUEST);
// } catch (IOException ex) {
// throw new WebApplicationException(ex);
// } catch (InternalRepositoryException ex) {
// throw new WebApplicationException(ex);
// }
// } else {
// throw new WebApplicationException(Response.Status.BAD_REQUEST);
// }
// }
private Type type(String type) {
RepositoryHandler handler = manager.getHandler(type);
@@ -574,18 +557,17 @@ public class RepositoryImportResource {
return handler.getType();
}
/**
* Request for importing external repositories which are accessible via url.
*/
@XmlRootElement(name = "import")
@XmlAccessorType(XmlAccessType.FIELD)
@Value
@ToString
public static class UrlImportRequest {
private String namespace;
private String name;
@Getter
@Setter
@NoArgsConstructor
@SuppressWarnings("java:S2160")
public static class RepositoryImportDto extends RepositoryDto {
private String url;
private String username;
private String password;
RepositoryImportDto(Links links, Embedded embedded) {
super(links, embedded);
}
}
}