From e3a2783bfdf77aae1a2053acf19196878090c8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 4 Sep 2020 21:44:48 +0200 Subject: [PATCH] Get links for repositories by namespaces from server --- scm-ui/ui-types/src/Repositories.ts | 7 ++ scm-ui/ui-types/src/index.ts | 2 +- scm-ui/ui-webapp/src/modules/indexResource.ts | 4 + .../src/repos/containers/Overview.tsx | 56 ++++++++++--- scm-ui/ui-webapp/src/repos/modules/repos.ts | 83 +++++++++++++++++-- .../api/v2/resources/IndexDtoGenerator.java | 1 + .../NamespaceToNamespaceDtoMapper.java | 9 +- .../scm/api/v2/resources/ResourceLinks.java | 4 + 8 files changed, 145 insertions(+), 21 deletions(-) diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 7b9d54a3a8..921ec4bba5 100644 --- a/scm-ui/ui-types/src/Repositories.ts +++ b/scm-ui/ui-types/src/Repositories.ts @@ -39,12 +39,19 @@ export type RepositoryCreation = Repository & { contextEntries: { [key: string]: any }; }; +export type Namespace = { + namespace: string; + _links: Links; +}; + export type RepositoryCollection = PagedCollection & { _embedded: { repositories: Repository[] | string[]; }; }; +export type NamespaceCollection = Namespace[]; + export type RepositoryGroup = { name: string; repositories: Repository[]; diff --git a/scm-ui/ui-types/src/index.ts b/scm-ui/ui-types/src/index.ts index f0263afe3b..35adda77bb 100644 --- a/scm-ui/ui-types/src/index.ts +++ b/scm-ui/ui-types/src/index.ts @@ -29,7 +29,7 @@ export { Me } from "./Me"; export { DisplayedUser, User } from "./User"; export { Group, Member } from "./Group"; -export { Repository, RepositoryCollection, RepositoryGroup, RepositoryCreation } from "./Repositories"; +export { Repository, RepositoryCollection, RepositoryGroup, RepositoryCreation, Namespace, NamespaceCollection } from "./Repositories"; export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes"; export { Branch, BranchRequest } from "./Branches"; diff --git a/scm-ui/ui-webapp/src/modules/indexResource.ts b/scm-ui/ui-webapp/src/modules/indexResource.ts index 4d642c5fb4..b204e5600f 100644 --- a/scm-ui/ui-webapp/src/modules/indexResource.ts +++ b/scm-ui/ui-webapp/src/modules/indexResource.ts @@ -234,6 +234,10 @@ export function getRepositoriesLink(state: object) { return getLink(state, "repositories"); } +export function getNamespacesLink(state: object) { + return getLink(state, "namespaces"); +} + export function getHgConfigLink(state: object) { return getLink(state, "hgConfig"); } diff --git a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx index 09cb66defb..60f70e87fd 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx @@ -25,7 +25,7 @@ import React from "react"; import { connect } from "react-redux"; import { RouteComponentProps, withRouter } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; -import { RepositoryCollection } from "@scm-manager/ui-types"; +import { NamespaceCollection, RepositoryCollection } from "@scm-manager/ui-types"; import { CreateButton, LinkPaginator, @@ -35,13 +35,16 @@ import { PageActions, urls } from "@scm-manager/ui-components"; -import { getRepositoriesLink } from "../../modules/indexResource"; +import { getNamespacesLink, getRepositoriesLink } from "../../modules/indexResource"; import { fetchReposByPage, getFetchReposFailure, getRepositoryCollection, isAbleToCreateRepos, - isFetchReposPending + isFetchReposPending, + isFetchNamespacesPending, + getNamespaceCollection, + fetchNamespaces } from "../modules/repos"; import RepositoryList from "../components/list"; @@ -51,30 +54,52 @@ type Props = WithTranslation & error: Error; showCreateButton: boolean; collection: RepositoryCollection; + namespaces: NamespaceCollection; page: number; namespace: string; reposLink: string; + namespacesLink: string; // dispatched functions fetchReposByPage: (link: string, page: number, namespace?: string, filter?: string) => void; + fetchNamespaces: (link: string) => void; }; class Overview extends React.Component { componentDidMount() { - const { fetchReposByPage, reposLink, namespace, page, location } = this.props; - fetchReposByPage(reposLink, page, namespace, urls.getQueryStringFromLocation(location)); + const { fetchReposByPage, fetchNamespaces, namespacesLink, namespace, page, location } = this.props; + fetchNamespaces(namespacesLink); + const link = this.getReposLink(); + if (link) { + fetchReposByPage(link, page, namespace, urls.getQueryStringFromLocation(location)); + } } componentDidUpdate = (prevProps: Props) => { - const { loading, collection, namespace, page, reposLink, location, fetchReposByPage } = this.props; - if (collection && page && !loading) { + const { loading, collection, namespaces, namespace, page, location, fetchReposByPage } = this.props; + if (!collection && namespace && prevProps.namespaces !== namespaces) { + const link = this.getReposLink(); + fetchReposByPage(link, page, namespace, urls.getQueryStringFromLocation(location)); + } else if (collection && page && !loading) { const statePage: number = collection.page + 1; if (page !== statePage || prevProps.location.search !== location.search) { - fetchReposByPage(reposLink, page, namespace, urls.getQueryStringFromLocation(location)); + const link = this.getReposLink(); + if (link) { + fetchReposByPage(link, page, namespace, urls.getQueryStringFromLocation(location)); + } } } }; + getReposLink = () => { + const { namespace, namespaces, reposLink } = this.props; + if (namespace) { + return namespaces?.find(n => n.namespace === namespace)?._links?.repositories?.href; + } else { + return reposLink; + } + }; + render() { const { error, loading, showCreateButton, namespace, t } = this.props; @@ -134,26 +159,33 @@ class Overview extends React.Component { const mapStateToProps = (state: any, ownProps: Props) => { const { match } = ownProps; const collection = getRepositoryCollection(state); - const loading = isFetchReposPending(state); + const namespaces = getNamespaceCollection(state); + const loading = isFetchReposPending(state) || isFetchNamespacesPending(state); const error = getFetchReposFailure(state); const { namespace, page } = urls.getNamespaceAndPageFromMatch(match); const showCreateButton = isAbleToCreateRepos(state); const reposLink = getRepositoriesLink(state); + const namespacesLink = getNamespacesLink(state); return { collection, + namespaces, loading, error, page, namespace, showCreateButton, - reposLink + reposLink, + namespacesLink }; }; const mapDispatchToProps = (dispatch: any) => { return { - fetchReposByPage: (link: string, page: number, namespace?: string, filter?: string) => { - dispatch(fetchReposByPage(link, page, namespace, filter)); + fetchReposByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchReposByPage(link, page, filter)); + }, + fetchNamespaces: (link: string) => { + dispatch(fetchNamespaces(link)); } }; }; diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index 6c56748e98..0b3af79575 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -24,7 +24,7 @@ import { apiClient } from "@scm-manager/ui-components"; import * as types from "../../modules/types"; -import { Action, Repository, RepositoryCollection, RepositoryCreation } from "@scm-manager/ui-types"; +import { Action, Repository, RepositoryCollection, RepositoryCreation, NamespaceCollection } from "@scm-manager/ui-types"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; @@ -33,6 +33,11 @@ export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`; export const FETCH_REPOS_SUCCESS = `${FETCH_REPOS}_${types.SUCCESS_SUFFIX}`; export const FETCH_REPOS_FAILURE = `${FETCH_REPOS}_${types.FAILURE_SUFFIX}`; +export const FETCH_NAMESPACES = "scm/repos/FETCH_NAMESPACES"; +export const FETCH_NAMESPACES_PENDING = `${FETCH_NAMESPACES}_${types.PENDING_SUFFIX}`; +export const FETCH_NAMESPACES_SUCCESS = `${FETCH_NAMESPACES}_${types.SUCCESS_SUFFIX}`; +export const FETCH_NAMESPACES_FAILURE = `${FETCH_NAMESPACES}_${types.FAILURE_SUFFIX}`; + export const FETCH_REPO = "scm/repos/FETCH_REPO"; export const FETCH_REPO_PENDING = `${FETCH_REPO}_${types.PENDING_SUFFIX}`; export const FETCH_REPO_SUCCESS = `${FETCH_REPO}_${types.SUCCESS_SUFFIX}`; @@ -67,11 +72,10 @@ export function fetchRepos(link: string) { return fetchReposByLink(link); } -export function fetchReposByPage(link: string, page: number, namespace?: string, filter?: string) { - const namespacePath = namespace ? `${namespace}/` : ""; - const linkWithPage = `${link}${namespacePath}?page=${page - 1}`; +export function fetchReposByPage(link: string, page: number, filter?: string) { + const linkWithPage = `${link}?page=${page - 1}`; if (filter) { - return fetchReposByLink(`${linkWithPage}}&q=${decodeURIComponent(filter)}`); + return fetchReposByLink(`${linkWithPage}&q=${decodeURIComponent(filter)}`); } return fetchReposByLink(linkWithPage); } @@ -125,6 +129,42 @@ export function fetchReposFailure(err: Error): Action { }; } +// fetch namespaces +export function fetchNamespaces(link: string) { + return function(dispatch: any) { + dispatch(fetchNamespacesPending()); + return apiClient + .get(link) + .then(response => response.json()) + .then(namespaces => { + dispatch(fetchNamespacesSuccess(namespaces)); + }) + .catch(err => { + dispatch(fetchNamespacesFailure(err)); + }); + }; +} + +export function fetchNamespacesPending(): Action { + return { + type: FETCH_NAMESPACES_PENDING + }; +} + +export function fetchNamespacesSuccess(namespaces: NamespaceCollection): Action { + return { + type: FETCH_NAMESPACES_SUCCESS, + payload: namespaces + }; +} + +export function fetchNamespacesFailure(err: Error): Action { + return { + type: FETCH_NAMESPACES_FAILURE, + payload: err + }; +} + // fetch repo export function fetchRepoByLink(repo: Repository) { return fetchRepo(repo._links.self.href, repo.namespace, repo.name); @@ -348,7 +388,7 @@ function createIdentifier(repository: Repository) { return repository.namespace + "/" + repository.name; } -function normalizeByNamespaceAndName(repositoryCollection: RepositoryCollection) { +function normalizeByNamespaceAndName(state: object, repositoryCollection: RepositoryCollection) { const names = []; const byNames = {}; for (const repository of repositoryCollection._embedded.repositories) { @@ -357,6 +397,7 @@ function normalizeByNamespaceAndName(repositoryCollection: RepositoryCollection) byNames[identifier] = repository; } return { + ...state, list: { ...repositoryCollection, _embedded: { @@ -378,6 +419,13 @@ const reducerByNames = (state: object, repository: Repository) => { }; }; +const reducerForNamespaces = (state: object, namespaces: NamespaceCollection) => { + return { + ...state, + namespaces: namespaces._embedded + }; +}; + export default function reducer( state: object = {}, action: Action = { @@ -390,7 +438,9 @@ export default function reducer( switch (action.type) { case FETCH_REPOS_SUCCESS: - return normalizeByNamespaceAndName(action.payload); + return normalizeByNamespaceAndName(state, action.payload); + case FETCH_NAMESPACES_SUCCESS: + return reducerForNamespaces(state, action.payload); case FETCH_REPO_SUCCESS: return reducerByNames(state, action.payload); default: @@ -408,6 +458,7 @@ export function getRepositoryCollection(state: object) { } return { ...state.repos.list, + ...state.repos.namespaces, _embedded: { repositories } @@ -415,6 +466,10 @@ export function getRepositoryCollection(state: object) { } } +export function getNamespaceCollection(state: object) { + return state.repos.namespaces?.namespaces; +} + export function isFetchReposPending(state: object) { return isPending(state, FETCH_REPOS); } @@ -429,6 +484,20 @@ export function getRepository(state: object, namespace: string, name: string) { } } +export function isFetchNamespacesPending(state: object) { + return isPending(state, FETCH_NAMESPACES); +} + +export function getFetchNamespacesFailure(state: object) { + return getFailure(state, FETCH_NAMESPACES); +} + +export function getNamespace(state: object, namespace: string) { + if (state.namespaces) { + return state.namespaces[namespace]; + } +} + export function isFetchRepoPending(state: object, namespace: string, name: string) { return isPending(state, FETCH_REPO, namespace + "/" + name); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 1339c55097..d8c9acc7f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -104,6 +104,7 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.single(link("config", resourceLinks.config().self())); } builder.single(link("repositories", resourceLinks.repositoryCollection().self())); + builder.single(link("namespaces", resourceLinks.namespaceCollection().self())); if (PermissionPermissions.list().isPermitted()) { builder.single(link("permissions", resourceLinks.permissions().self())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceToNamespaceDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceToNamespaceDtoMapper.java index 53e42de3cf..7a464299ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceToNamespaceDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/NamespaceToNamespaceDtoMapper.java @@ -26,6 +26,7 @@ package sonia.scm.api.v2.resources; import javax.inject.Inject; +import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; class NamespaceToNamespaceDtoMapper { @@ -38,6 +39,12 @@ class NamespaceToNamespaceDtoMapper { } NamespaceDto map(String namespace) { - return new NamespaceDto(namespace, linkingTo().self(links.namespace().self(namespace)).build()); + return new NamespaceDto( + namespace, + linkingTo() + .self(links.namespace().self(namespace)) + .single(link("repositories", links.repositoryCollection().forNamespace(namespace))) + .build() + ); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 0e44ba82f4..3dd2180ede 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -911,5 +911,9 @@ class ResourceLinks { String self(String namespace) { return namespaceLinkBuilder.method("getNamespaceResource").parameters().method("get").parameters(namespace).href(); } + + String repositories(String namespace) { + return namespaceLinkBuilder.method("getNamespaceResource").parameters().method("get").parameters(namespace).href(); + } } }