mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-04 11:20:53 +01:00
merge
This commit is contained in:
@@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Tags overview for repository [#1331](https://github.com/scm-manager/scm-manager/pull/1331)
|
||||
- Permissions can be specified for namespaces ([#1335](https://github.com/scm-manager/scm-manager/pull/1335))
|
||||
|
||||
### Fixed
|
||||
- Missing synchronization during repository creation ([#1328](https://github.com/scm-manager/scm-manager/pull/1328))
|
||||
- Missing BranchCreatedEvent for mercurial ([#1334](https://github.com/scm-manager/scm-manager/pull/1334))
|
||||
|
||||
BIN
docs/de/user/repo/assets/repository-tag-detailView.png
Normal file
BIN
docs/de/user/repo/assets/repository-tag-detailView.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/de/user/repo/assets/repository-tags-overview.png
Normal file
BIN
docs/de/user/repo/assets/repository-tags-overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -6,6 +6,7 @@ partiallyActive: true
|
||||
Der Bereich Repository umfasst alles auf Basis von Repositories in Namespaces. Dazu zählen alle Operationen auf Branches, der Code und Einstellungen.
|
||||
|
||||
* [Branches](branches/)
|
||||
* [Tags](tags/)
|
||||
* [Code](code/)
|
||||
* [Einstellungen](settings/)
|
||||
<!--- AppendLinkContentEnd -->
|
||||
|
||||
13
docs/de/user/repo/tags.md
Normal file
13
docs/de/user/repo/tags.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Repository
|
||||
subtitle: Tags
|
||||
---
|
||||
### Übersicht
|
||||
Auf der Tags-Übersicht sind die existierenden Tags nach Erstelldatum absteigend aufgeführt. Bei einem Klick auf einen Tag wird der Benutzer zur Detailseite des Tags weitergeleitet.
|
||||
|
||||

|
||||
|
||||
### Tag Detailseite
|
||||
Hier wird ein Befehl zum Arbeiten mit dem Tag auf einer Kommandozeile aufgeführt.
|
||||
|
||||

|
||||
BIN
docs/en/user/repo/assets/repository-tag-detailView.png
Normal file
BIN
docs/en/user/repo/assets/repository-tag-detailView.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/en/user/repo/assets/repository-tags-overview.png
Normal file
BIN
docs/en/user/repo/assets/repository-tags-overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -5,6 +5,7 @@ partiallyActive: true
|
||||
The Repository area includes everything based on repositories in namespaces. This includes all operations on branches, the code and settings.
|
||||
|
||||
* [Branches](branches/)
|
||||
* [Tags](tags/)
|
||||
* [Code](code/)
|
||||
* [Settings](settings/)
|
||||
|
||||
|
||||
13
docs/en/user/repo/tags.md
Normal file
13
docs/en/user/repo/tags.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Repository
|
||||
subtitle: Tags
|
||||
---
|
||||
### Overview
|
||||
The tag overview shows the tags that exist for this repository. By clicking on a tag, the details page of the tag is shown.
|
||||
|
||||

|
||||
|
||||
### Tag Details Page
|
||||
This page shows a command to work with the tag on the command line.
|
||||
|
||||

|
||||
@@ -109,6 +109,14 @@ public class Namespace implements PermissionObject, Cloneable {
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Namespace{" +
|
||||
"namespace='" + namespace + '\'' +
|
||||
", permissions=" + permissions +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public Namespace clone() {
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,13 @@ package sonia.scm.repository;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Manages namespaces. Mind that namespaces do not have a lifecycle on their own, but only do exist through
|
||||
* repositories. Therefore you cannot create or delete namespaces, but just change related settings like permissions
|
||||
* associated with them.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public interface NamespaceManager {
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,11 +119,16 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
final RepositoryPermission other = (RepositoryPermission) obj;
|
||||
|
||||
return Objects.equal(name, other.name)
|
||||
&& (verbs == null && other.verbs == null || verbs != null && other.verbs != null && CollectionUtils.isEqualCollection(verbs, other.verbs))
|
||||
&& equalVerbs(other)
|
||||
&& Objects.equal(role, other.role)
|
||||
&& Objects.equal(groupPermission, other.groupPermission);
|
||||
}
|
||||
|
||||
public boolean equalVerbs(RepositoryPermission other) {
|
||||
return verbs == null && other.verbs == null
|
||||
|| verbs != null && other.verbs != null && CollectionUtils.isEqualCollection(verbs, other.verbs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hash code value for the {@link RepositoryPermission}.
|
||||
*
|
||||
|
||||
48
scm-plugins/scm-git-plugin/src/main/js/GitTagInformation.tsx
Normal file
48
scm-plugins/scm-git-plugin/src/main/js/GitTagInformation.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { Tag } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
tag: Tag;
|
||||
};
|
||||
|
||||
const GitTagInformation: FC<Props> = ({ tag }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{t("scm-git-plugin.information.checkoutTag")}</h4>
|
||||
<pre>
|
||||
<code>
|
||||
git checkout tags/{tag.name} -b branch/{tag.name}
|
||||
</code>
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitTagInformation;
|
||||
@@ -32,6 +32,7 @@ import GitGlobalConfiguration from "./GitGlobalConfiguration";
|
||||
import GitBranchInformation from "./GitBranchInformation";
|
||||
import GitMergeInformation from "./GitMergeInformation";
|
||||
import RepositoryConfig from "./RepositoryConfig";
|
||||
import GitTagInformation from "./GitTagInformation";
|
||||
|
||||
// repository
|
||||
|
||||
@@ -42,6 +43,7 @@ export const gitPredicate = (props: any) => {
|
||||
|
||||
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
|
||||
binder.bind("repos.branch-details.information", GitBranchInformation, gitPredicate);
|
||||
binder.bind("repos.tag-details.information", GitTagInformation, gitPredicate);
|
||||
binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate);
|
||||
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"replace": "Ein bestehendes Repository aktualisieren",
|
||||
"fetch": "Remote-Änderungen herunterladen",
|
||||
"checkout": "Branch wechseln",
|
||||
"checkoutTag": "Tag als neuen Branch auschecken",
|
||||
"merge": {
|
||||
"heading": "Merge des Source Branch in den Target Branch",
|
||||
"checkout": "1. Sicherstellen, dass der Workspace aufgeräumt ist und der Target Branch ausgecheckt wurde.",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"replace": "Push an existing repository",
|
||||
"fetch": "Get remote changes",
|
||||
"checkout": "Switch branch",
|
||||
"checkoutTag": "Checkout tag as new branch",
|
||||
"merge": {
|
||||
"heading": "How to merge source branch into target branch",
|
||||
"checkout": "1. Make sure your workspace is clean and checkout target branch",
|
||||
|
||||
46
scm-plugins/scm-hg-plugin/src/main/js/HgTagInformation.tsx
Normal file
46
scm-plugins/scm-hg-plugin/src/main/js/HgTagInformation.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tag } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
tag: Tag;
|
||||
};
|
||||
|
||||
const HgTagInformation: FC<Props> = ({ tag }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{t("scm-hg-plugin.information.checkoutTag")}</h4>
|
||||
<pre>
|
||||
<code>hg update {tag.name}</code>
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HgTagInformation;
|
||||
@@ -28,6 +28,7 @@ import HgAvatar from "./HgAvatar";
|
||||
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
|
||||
import HgGlobalConfiguration from "./HgGlobalConfiguration";
|
||||
import HgBranchInformation from "./HgBranchInformation";
|
||||
import HgTagInformation from "./HgTagInformation";
|
||||
|
||||
const hgPredicate = (props: any) => {
|
||||
return props.repository && props.repository.type === "hg";
|
||||
@@ -35,6 +36,7 @@ const hgPredicate = (props: any) => {
|
||||
|
||||
binder.bind("repos.repository-details.information", ProtocolInformation, hgPredicate);
|
||||
binder.bind("repos.branch-details.information", HgBranchInformation, hgPredicate);
|
||||
binder.bind("repos.tag-details.information", HgTagInformation, hgPredicate);
|
||||
binder.bind("repos.repository-avatar", HgAvatar, hgPredicate);
|
||||
|
||||
// bind global configuration
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"create" : "Neues Repository erstellen",
|
||||
"replace" : "Ein bestehendes Repository aktualisieren",
|
||||
"fetch": "Remote-Änderungen herunterladen",
|
||||
"checkout": "Branch wechseln"
|
||||
"checkout": "Branch wechseln",
|
||||
"checkoutTag": "Tag auschecken"
|
||||
},
|
||||
"config": {
|
||||
"link": "Mercurial",
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"create" : "Create a new repository",
|
||||
"replace" : "Push an existing repository",
|
||||
"fetch": "Get remote changes",
|
||||
"checkout": "Switch branch"
|
||||
"checkout": "Switch branch",
|
||||
"checkoutTag": "Checkout tag"
|
||||
},
|
||||
"config": {
|
||||
"link": "Mercurial",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
import queryString from "query-string";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
|
||||
//@ts-ignore
|
||||
export const contextPath = window.ctxPath || "";
|
||||
@@ -80,3 +81,19 @@ function parsePageNumber(pageAsString: string) {
|
||||
export function getQueryStringFromLocation(location: any) {
|
||||
return location.search ? queryString.parse(location.search).q : undefined;
|
||||
}
|
||||
|
||||
export function stripEndingSlash(url: string) {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function matchedUrlFromMatch(match: any) {
|
||||
return stripEndingSlash(match.url);
|
||||
}
|
||||
|
||||
export function matchedUrl(props: RouteComponentProps) {
|
||||
const match = props.match;
|
||||
return matchedUrlFromMatch(match);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,6 @@ export type NamespaceCollection = {
|
||||
|
||||
export type RepositoryGroup = {
|
||||
name: string;
|
||||
namespace: Namespace;
|
||||
namespace?: Namespace;
|
||||
repositories: Repository[];
|
||||
};
|
||||
|
||||
@@ -27,5 +27,6 @@ import { Links } from "./hal";
|
||||
export type Tag = {
|
||||
name: string;
|
||||
revision: string;
|
||||
date?: Date;
|
||||
_links: Links;
|
||||
};
|
||||
|
||||
@@ -5,5 +5,10 @@
|
||||
"settingsNavLink": "Einstellungen",
|
||||
"permissionsNavLink": "Berechtigungen"
|
||||
}
|
||||
},
|
||||
"repositoryOverview": {
|
||||
"settings": {
|
||||
"tooltip": "Einstellungen für den Namespace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"navigationLabel": "Repository",
|
||||
"informationNavLink": "Informationen",
|
||||
"branchesNavLink": "Branches",
|
||||
"tagsNavLink": "Tags",
|
||||
"sourcesNavLink": "Code",
|
||||
"settingsNavLink": "Einstellungen",
|
||||
"generalNavLink": "Generell",
|
||||
@@ -69,6 +70,21 @@
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default"
|
||||
},
|
||||
"tags": {
|
||||
"overview": {
|
||||
"title": "Übersicht aller verfügbaren Tags",
|
||||
"noTags": "Keine Tags gefunden.",
|
||||
"created": "Erstellt"
|
||||
},
|
||||
"table": {
|
||||
"tags": "Tags"
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"name": "Name",
|
||||
"commit": "Commit",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"code": {
|
||||
"sources": "Sources",
|
||||
"commits": "Commits",
|
||||
|
||||
@@ -5,5 +5,10 @@
|
||||
"settingsNavLink": "Settings",
|
||||
"permissionsNavLink": "Permissions"
|
||||
}
|
||||
},
|
||||
"repositoryOverview": {
|
||||
"settings": {
|
||||
"tooltip": "Namespace related settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"navigationLabel": "Repository",
|
||||
"informationNavLink": "Information",
|
||||
"branchesNavLink": "Branches",
|
||||
"tagsNavLink": "Tags",
|
||||
"sourcesNavLink": "Code",
|
||||
"settingsNavLink": "Settings",
|
||||
"generalNavLink": "General",
|
||||
@@ -69,6 +70,21 @@
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default"
|
||||
},
|
||||
"tags": {
|
||||
"overview": {
|
||||
"title": "Overview of all tags",
|
||||
"noTags": "No tags found.",
|
||||
"created": "Created"
|
||||
},
|
||||
"table": {
|
||||
"tags": "Tags"
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"name": "Name",
|
||||
"commit": "Commit",
|
||||
"sources": "Sources"
|
||||
},
|
||||
"code": {
|
||||
"sources": "Sources",
|
||||
"commits": "Commits",
|
||||
|
||||
@@ -45,6 +45,7 @@ import GlobalConfig from "./GlobalConfig";
|
||||
import RepositoryRoles from "../roles/containers/RepositoryRoles";
|
||||
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
|
||||
import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -54,30 +55,17 @@ type Props = RouteComponentProps &
|
||||
};
|
||||
|
||||
class Admin extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
if (url.includes("role")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
matchesRoles = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}/role/`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { links, availablePluginsLink, installedPluginsLink, t } = this.props;
|
||||
const { links, availablePluginsLink, installedPluginsLink, match, t } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const extensionProps = {
|
||||
links,
|
||||
url
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { History } from "history";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
@@ -34,39 +34,29 @@ import { fetchRoleByName, getFetchRoleFailure, getRoleByName, isFetchRolePending
|
||||
import PermissionRoleDetail from "../components/PermissionRoleDetails";
|
||||
import EditRepositoryRole from "./EditRepositoryRole";
|
||||
import { compose } from "redux";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
roleName: string;
|
||||
role: RepositoryRole;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
repositoryRolesLink: string;
|
||||
disabled: boolean;
|
||||
type Props = WithTranslation &
|
||||
RouteComponentProps & {
|
||||
roleName: string;
|
||||
role: RepositoryRole;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
repositoryRolesLink: string;
|
||||
disabled: boolean;
|
||||
|
||||
// dispatcher function
|
||||
fetchRoleByName: (p1: string, p2: string) => void;
|
||||
// dispatcher function
|
||||
fetchRoleByName: (p1: string, p2: string) => void;
|
||||
|
||||
// context objects
|
||||
match: any;
|
||||
history: History;
|
||||
};
|
||||
// context objects
|
||||
history: History;
|
||||
};
|
||||
|
||||
class SingleRepositoryRole extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchRoleByName(this.props.repositoryRolesLink, this.props.roleName);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, role } = this.props;
|
||||
|
||||
@@ -80,7 +70,7 @@ class SingleRepositoryRole extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
role,
|
||||
|
||||
@@ -44,6 +44,7 @@ import ProfileInfo from "./ProfileInfo";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys";
|
||||
import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -54,17 +55,6 @@ type Props = RouteComponentProps &
|
||||
};
|
||||
|
||||
class Profile extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
mayChangePassword = () => {
|
||||
const { me } = this.props;
|
||||
return !!me?._links?.password;
|
||||
@@ -76,7 +66,7 @@ class Profile extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const { me, t } = this.props;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import { Details } from "./../components/table";
|
||||
import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks";
|
||||
import EditGroup from "./EditGroup";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -63,17 +64,6 @@ class SingleGroup extends React.Component<Props> {
|
||||
this.props.fetchGroupByName(this.props.groupLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, group } = this.props;
|
||||
|
||||
@@ -85,7 +75,7 @@ class SingleGroup extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
group,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Branch } from "@scm-manager/ui-types";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
@@ -31,24 +31,18 @@ type Props = {
|
||||
branch: Branch;
|
||||
};
|
||||
|
||||
class BranchRow extends React.Component<Props> {
|
||||
renderLink(to: string, label: string, defaultBranch?: boolean) {
|
||||
return (
|
||||
<Link to={to} title={label}>
|
||||
{label} <DefaultBranchTag defaultBranch={defaultBranch} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { baseUrl, branch } = this.props;
|
||||
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
||||
return (
|
||||
<tr>
|
||||
<td>{this.renderLink(to, branch.name, branch.defaultBranch)}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
const BranchRow: FC<Props> = ({ baseUrl, branch }) => {
|
||||
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Link to={to} title={branch.name}>
|
||||
{branch.name}
|
||||
<DefaultBranchTag defaultBranch={branch.defaultBranch} />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default BranchRow;
|
||||
|
||||
@@ -31,6 +31,7 @@ import { fetchBranch, getBranch, getFetchBranchFailure, isFetchBranchPending } f
|
||||
import { ErrorNotification, Loading, NotFoundError } from "@scm-manager/ui-components";
|
||||
import { History } from "history";
|
||||
import queryString from "query-string";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -54,21 +55,10 @@ class BranchRoot extends React.Component<Props> {
|
||||
fetchBranch(repository, branchName);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, branch, loading, error, match, location } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof NotFoundError && queryString.parse(location.search).create === "true") {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import { FAILURE_SUFFIX, PENDING_SUFFIX, RESET_SUFFIX, SUCCESS_SUFFIX } from "../../../modules/types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { Action, Branch, BranchRequest, Repository } from "@scm-manager/ui-types";
|
||||
import { Action, Branch, BranchRequest, Repository, Link } from "@scm-manager/ui-types";
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
|
||||
@@ -65,7 +65,7 @@ export function fetchBranches(repository: Repository) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchBranchesPending(repository));
|
||||
return apiClient
|
||||
.get(repository._links.branches.href)
|
||||
.get((repository._links.branches as Link).href)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchBranchesSuccess(data, repository));
|
||||
@@ -77,7 +77,7 @@ export function fetchBranches(repository: Repository) {
|
||||
}
|
||||
|
||||
export function fetchBranch(repository: Repository, name: string) {
|
||||
let link = repository._links.branches.href;
|
||||
let link = (repository._links.branches as Link).href;
|
||||
if (!link.endsWith("/")) {
|
||||
link += "/";
|
||||
}
|
||||
|
||||
@@ -26,17 +26,18 @@ import { Link } from "react-router-dom";
|
||||
import { CardColumnGroup, RepositoryEntry } from "@scm-manager/ui-components";
|
||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||
import { Icon } from "@scm-manager/ui-components";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
type Props = WithTranslation & {
|
||||
group: RepositoryGroup;
|
||||
};
|
||||
|
||||
class RepositoryGroupEntry extends React.Component<Props> {
|
||||
render() {
|
||||
const { group } = this.props;
|
||||
const { group, t } = this.props;
|
||||
const settingsLink = group.namespace?._links?.permissions && (
|
||||
<Link to={`/namespace/${group.name}/settings`}>
|
||||
<Icon color={"is-link"} name={"cog"} />
|
||||
<Icon color={"is-link"} name={"cog"} title={t("repositoryOverview.settings.tooltip")} />
|
||||
</Link>
|
||||
);
|
||||
const namespaceHeader = (
|
||||
@@ -54,4 +55,4 @@ class RepositoryGroupEntry extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default RepositoryGroupEntry;
|
||||
export default withTranslation("namespaces")(RepositoryGroupEntry);
|
||||
|
||||
@@ -65,5 +65,5 @@ function sortByName(a, b) {
|
||||
}
|
||||
|
||||
function findNamespace(namespaces: NamespaceCollection, namespaceToFind: string) {
|
||||
return namespaces._embedded.namespaces.filter(namespace => namespace.namespace === namespaceToFind)[0];
|
||||
return namespaces._embedded.namespaces.find(namespace => namespace.namespace === namespaceToFind);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import Changesets from "./Changesets";
|
||||
import { compose } from "redux";
|
||||
import CodeActionBar from "../codeSection/components/CodeActionBar";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation &
|
||||
RouteComponentProps & {
|
||||
@@ -38,13 +39,6 @@ type Props = WithTranslation &
|
||||
};
|
||||
|
||||
class ChangesetsRoot extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
isBranchAvailable = () => {
|
||||
const { branches, selectedBranch } = this.props;
|
||||
return branches?.filter(b => b.name === selectedBranch).length === 0;
|
||||
@@ -75,7 +69,7 @@ class ChangesetsRoot extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = this.stripEndingSlash(match.url);
|
||||
const url = urls.stripEndingSlash(match.url);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import RepositoryForm from "../components/form";
|
||||
import { Repository, Links } from "@scm-manager/ui-types";
|
||||
import { getModifyRepoFailure, isModifyRepoPending, modifyRepo, modifyRepoReset } from "../modules/repos";
|
||||
@@ -33,8 +33,9 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { compose } from "redux";
|
||||
import DangerZone from "./DangerZone";
|
||||
import { getLinks } from "../../modules/indexResource";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
type Props = RouteComponentProps & {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
indexLinks: Links;
|
||||
@@ -45,7 +46,6 @@ type Props = {
|
||||
// context props
|
||||
repository: Repository;
|
||||
history: History;
|
||||
match: any;
|
||||
};
|
||||
|
||||
class EditRepo extends React.Component<Props> {
|
||||
@@ -59,21 +59,10 @@ class EditRepo extends React.Component<Props> {
|
||||
history.push(`/repo/${repository.namespace}/${repository.name}`);
|
||||
};
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, indexLinks } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
repository,
|
||||
|
||||
@@ -54,6 +54,9 @@ import CodeOverview from "../codeSection/containers/CodeOverview";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import SourceExtensions from "../sources/containers/SourceExtensions";
|
||||
import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components";
|
||||
import TagsOverview from "../tags/container/TagsOverview";
|
||||
import TagRoot from "../tags/container/TagRoot";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -82,25 +85,20 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
matchesBranches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}/branch/.+/info`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
matchesTags = (route: any) => {
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}/tag/.+/info`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
matchesCode = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}(/code)/.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
@@ -118,7 +116,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
|
||||
evaluateDestinationForCodeLink = () => {
|
||||
const { repository } = this.props;
|
||||
const url = `${this.matchedUrl()}/code`;
|
||||
const url = `${urls.matchedUrl(this.props)}/code`;
|
||||
if (repository?._links?.sources) {
|
||||
return `${url}/sources/`;
|
||||
}
|
||||
@@ -138,7 +136,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
repository,
|
||||
@@ -245,6 +243,15 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
render={() => <BranchesOverview repository={repository} baseUrl={`${url}/branch`} />}
|
||||
/>
|
||||
<Route path={`${url}/branches/create`} render={() => <CreateBranch repository={repository} />} />
|
||||
<Route
|
||||
path={`${url}/tag/:tag`}
|
||||
render={() => <TagRoot repository={repository} baseUrl={`${url}/tag`} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/tags`}
|
||||
exact={true}
|
||||
render={() => <TagsOverview repository={repository} baseUrl={`${url}/tag`} />}
|
||||
/>
|
||||
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
|
||||
</Switch>
|
||||
</PrimaryContentColumn>
|
||||
@@ -267,6 +274,16 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
activeOnlyWhenExact={false}
|
||||
title={t("repositoryRoot.menu.branchesNavLink")}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="tags"
|
||||
to={`${url}/tags/`}
|
||||
icon="fas fa-tags"
|
||||
label={t("repositoryRoot.menu.tagsNavLink")}
|
||||
activeWhenMatch={this.matchesTags}
|
||||
activeOnlyWhenExact={false}
|
||||
title={t("repositoryRoot.menu.tagsNavLink")}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName={this.getCodeLinkname()}
|
||||
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
Loading,
|
||||
Page, PrimaryContentColumn,
|
||||
Page,
|
||||
PrimaryContentColumn,
|
||||
SecondaryNavigation,
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
import Permissions from "../../permissions/containers/Permissions";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -49,6 +51,7 @@ type Props = RouteComponentProps &
|
||||
namespaceName: string;
|
||||
namespacesLink: string;
|
||||
namespace: Namespace;
|
||||
error: Error;
|
||||
|
||||
// dispatch functions
|
||||
fetchNamespace: (link: string, namespace: string) => void;
|
||||
@@ -60,20 +63,9 @@ class NamespaceRoot extends React.Component<Props> {
|
||||
fetchNamespace(namespacesLink, namespaceName);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, namespaceName, namespace, t } = this.props;
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
namespace,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { Tag, Repository } from "@scm-manager/ui-types";
|
||||
import { Button, ButtonAddons } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
tag: Tag;
|
||||
};
|
||||
|
||||
const TagButtonGroup: FC<Props> = ({ repository, tag }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const changesetLink = `/repo/${repository.namespace}/${repository.name}/code/changeset/${encodeURIComponent(
|
||||
tag.revision
|
||||
)}`;
|
||||
const sourcesLink = `/repo/${repository.namespace}/${repository.name}/sources/${encodeURIComponent(tag.revision)}/`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonAddons>
|
||||
<Button link={changesetLink} icon="exchange-alt" label={t("tag.commit")} reducedMobile={true} />
|
||||
<Button link={sourcesLink} icon="code" label={t("tag.sources")} reducedMobile={true} />
|
||||
</ButtonAddons>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagButtonGroup;
|
||||
73
scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx
Normal file
73
scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository, Tag } from "@scm-manager/ui-types";
|
||||
import { DateFromNow, Level } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
import TagButtonGroup from "./TagButtonGroup";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
tag: Tag;
|
||||
};
|
||||
|
||||
const FlexRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Created = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
const Label = styled.strong`
|
||||
margin-right: 0.3rem;
|
||||
`;
|
||||
|
||||
const Date = styled(DateFromNow)`
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
const TagDetail: FC<Props> = ({ tag, repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
return (
|
||||
<div className="media">
|
||||
<FlexRow className="media-content subtitle">
|
||||
<Label>{t("tag.name") + ": "} </Label> {tag.name}
|
||||
<Created className="is-ellipsis-overflow">
|
||||
{t("tags.overview.created")} <Date date={tag.date} className="has-text-grey" />
|
||||
</Created>
|
||||
</FlexRow>
|
||||
<div className="media-right">
|
||||
<TagButtonGroup repository={repository} tag={tag} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagDetail;
|
||||
60
scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx
Normal file
60
scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tag } from "@scm-manager/ui-types";
|
||||
import styled from "styled-components";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
tag: Tag;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const Created = styled.span`
|
||||
margin-left: 1rem;
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
const TagRow: FC<Props> = ({ tag, baseUrl }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`;
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Link to={to} title={tag.name}>
|
||||
{tag.name}
|
||||
<Created className="has-text-grey is-ellipsis-overflow">
|
||||
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
||||
</Created>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagRow;
|
||||
60
scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx
Normal file
60
scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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, { FC } from "react";
|
||||
import { Tag } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TagRow from "./TagRow";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
tags: Tag[];
|
||||
};
|
||||
|
||||
const TagTable: FC<Props> = ({ baseUrl, tags }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
const renderRow = () => {
|
||||
let rowContent = null;
|
||||
if (tags) {
|
||||
rowContent = tags.map((tag, index) => {
|
||||
return <TagRow key={index} baseUrl={baseUrl} tag={tag} />;
|
||||
});
|
||||
}
|
||||
return rowContent;
|
||||
};
|
||||
|
||||
return (
|
||||
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("tags.table.tags")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{renderRow()}</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagTable;
|
||||
54
scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx
Normal file
54
scm-ui/ui-webapp/src/repos/tags/components/TagView.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 React, { FC } from "react";
|
||||
import { Repository, Tag } from "@scm-manager/ui-types";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import TagDetail from "./TagDetail";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
tag: Tag;
|
||||
};
|
||||
|
||||
const TagView: FC<Props> = ({ repository, tag }) => {
|
||||
return (
|
||||
<>
|
||||
<TagDetail tag={tag} repository={repository} />
|
||||
<hr />
|
||||
<div className="content">
|
||||
<ExtensionPoint
|
||||
name="repos.tag-details.information"
|
||||
renderAll={true}
|
||||
props={{
|
||||
repository,
|
||||
tag
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagView;
|
||||
87
scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx
Normal file
87
scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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, { FC, useEffect, useState } from "react";
|
||||
import { Link, Repository, Tag } from "@scm-manager/ui-types";
|
||||
import { Redirect, Switch, useLocation, useRouteMatch, Route } from "react-router-dom";
|
||||
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TagView from "../components/TagView";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const TagRoot: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const match = useRouteMatch();
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [tag, setTag] = useState<Tag>();
|
||||
|
||||
useEffect(() => {
|
||||
const link = (repository._links?.tags as Link)?.href;
|
||||
if (link) {
|
||||
apiClient
|
||||
.get(link)
|
||||
.then(r => r.json())
|
||||
.then(r => setTags(r._embedded.tags))
|
||||
.catch(setError);
|
||||
}
|
||||
}, [repository]);
|
||||
|
||||
useEffect(() => {
|
||||
const tagName = decodeURIComponent(match?.params?.tag);
|
||||
const link = tags?.length > 0 && (tags.find(tag => tag.name === tagName)?._links.self as Link).href;
|
||||
if (link) {
|
||||
apiClient
|
||||
.get(link)
|
||||
.then(r => r.json())
|
||||
.then(setTag)
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
}
|
||||
}, [tags]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading || !tags) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = urls.matchedUrlFromMatch(match);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Redirect exact from={url} to={`${url}/info`} />
|
||||
<Route path={`${url}/info`} component={() => <TagView repository={repository} tag={tag} />} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagRoot;
|
||||
75
scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx
Normal file
75
scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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, { FC, useEffect, useState } from "react";
|
||||
import { Repository, Tag, Link } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Loading, Notification, Subtitle, apiClient } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import orderTags from "../orderTags";
|
||||
import TagTable from "../components/TagTable";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const link = (repository._links?.tags as Link)?.href;
|
||||
if (link) {
|
||||
setLoading(true);
|
||||
apiClient
|
||||
.get(link)
|
||||
.then(r => r.json())
|
||||
.then(r => setTags(r._embedded.tags))
|
||||
.then(() => setLoading(false))
|
||||
.catch(setError);
|
||||
}
|
||||
}, [repository]);
|
||||
|
||||
const renderTagsTable = () => {
|
||||
if (!loading && tags?.length > 0) {
|
||||
orderTags(tags);
|
||||
return <TagTable baseUrl={baseUrl} tags={tags} />;
|
||||
}
|
||||
return <Notification type="info">{t("tags.overview.noTags")}</Notification>;
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <>{renderTagsTable()}</>;
|
||||
};
|
||||
|
||||
export default TagsOverview;
|
||||
52
scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts
Normal file
52
scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 orderTags from "./orderTags";
|
||||
|
||||
const tag1 = {
|
||||
name: "tag1",
|
||||
revision: "revision1",
|
||||
date: new Date(2020, 1, 1),
|
||||
_links: {}
|
||||
};
|
||||
const tag2 = {
|
||||
name: "tag2",
|
||||
revision: "revision2",
|
||||
date: new Date(2020, 1, 3),
|
||||
_links: {}
|
||||
};
|
||||
const tag3 = {
|
||||
name: "tag3",
|
||||
revision: "revision3",
|
||||
date: new Date(2020, 1, 2),
|
||||
_links: {}
|
||||
};
|
||||
|
||||
describe("order tags", () => {
|
||||
it("should order tags descending by date", () => {
|
||||
const tags = [tag1, tag2, tag3];
|
||||
orderTags(tags);
|
||||
expect(tags).toEqual([tag2, tag3, tag1]);
|
||||
});
|
||||
});
|
||||
32
scm-ui/ui-webapp/src/repos/tags/orderTags.ts
Normal file
32
scm-ui/ui-webapp/src/repos/tags/orderTags.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// sort tags by date beginning with latest first
|
||||
import { Tag } from "@scm-manager/ui-types";
|
||||
|
||||
export default (tags: Tag[]) => {
|
||||
tags.sort((a, b) => {
|
||||
return new Date(b.date) - new Date(a.date);
|
||||
});
|
||||
};
|
||||
@@ -47,6 +47,7 @@ import { mustGetUsersLink } from "../../modules/indexResource";
|
||||
import SetUserPassword from "../components/SetUserPassword";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import SetPublicKeys from "../components/publicKeys/SetPublicKeys";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -65,17 +66,6 @@ class SingleUser extends React.Component<Props> {
|
||||
this.props.fetchUserByName(this.props.usersLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, user } = this.props;
|
||||
|
||||
@@ -87,7 +77,7 @@ class SingleUser extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
user,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* 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;
|
||||
@@ -31,17 +31,23 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
@Getter @Setter @NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we do not need this for dto
|
||||
public class BranchDto extends HalRepresentation {
|
||||
|
||||
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
|
||||
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
|
||||
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
|
||||
|
||||
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
|
||||
@NotEmpty
|
||||
@Length(min = 1, max = 100)
|
||||
@Pattern(regexp = VALID_BRANCH_NAMES)
|
||||
private String name;
|
||||
private String revision;
|
||||
private boolean defaultBranch;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* 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.google.common.base.Strings;
|
||||
@@ -120,7 +120,11 @@ public class BranchRootResource {
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("branch") String branchName) throws IOException {
|
||||
public Response get(
|
||||
@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("branch") String branchName
|
||||
) throws IOException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
@@ -293,7 +297,10 @@ public class BranchRootResource {
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
|
||||
public Response getAll(
|
||||
@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name
|
||||
) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
return Response.ok(branchCollectionToDtoMapper.map(repositoryService.getRepository(), branches.getBranches())).build();
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* 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;
|
||||
|
||||
@@ -60,10 +60,10 @@ import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
|
||||
@Slf4j
|
||||
public class NamespacePermissionResource {
|
||||
|
||||
private RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper;
|
||||
private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
|
||||
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
|
||||
private ResourceLinks resourceLinks;
|
||||
private final RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper;
|
||||
private final RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
|
||||
private final RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final NamespaceManager manager;
|
||||
|
||||
@Inject
|
||||
@@ -234,7 +234,6 @@ public class NamespacePermissionResource {
|
||||
public void update(@PathParam("namespace") String namespaceName,
|
||||
@PathParam("permission-name") String permissionName,
|
||||
@Valid RepositoryPermissionDto permission) {
|
||||
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
|
||||
Namespace namespace = load(namespaceName);
|
||||
String extractedPermissionName = getPermissionName(permissionName);
|
||||
if (!isPermissionExist(new RepositoryPermissionDto(extractedPermissionName, isGroupPermission(permissionName)), namespace)) {
|
||||
@@ -256,7 +255,7 @@ public class NamespacePermissionResource {
|
||||
}
|
||||
namespace.addPermission(newPermission);
|
||||
manager.modify(namespace);
|
||||
log.info("the permission with name: {} is updated.", permissionName);
|
||||
log.info("the permission with name: {} is updated to {}.", permissionName, permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,9 +294,9 @@ public class NamespacePermissionResource {
|
||||
}
|
||||
|
||||
private Predicate<RepositoryPermission> filterPermission(String name) {
|
||||
return permission -> getPermissionName(name).equals(permission.getName())
|
||||
&&
|
||||
permission.isGroupPermission() == isGroupPermission(name);
|
||||
return permission ->
|
||||
getPermissionName(name).equals(permission.getName())
|
||||
&& permission.isGroupPermission() == isGroupPermission(name);
|
||||
}
|
||||
|
||||
private String getPermissionName(String permissionName) {
|
||||
|
||||
@@ -56,16 +56,6 @@ public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper {
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Namespace namespace);
|
||||
|
||||
@BeforeMapping
|
||||
void validatePermissions(@Context Repository repository) {
|
||||
RepositoryPermissions.permissionRead(repository).check();
|
||||
}
|
||||
|
||||
@BeforeMapping
|
||||
void validatePermissions(@Context Namespace namespace) {
|
||||
NamespacePermissions.permissionRead().check();
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Repository repository) {
|
||||
String permissionName = getUrlPermissionName(target);
|
||||
|
||||
@@ -80,8 +80,7 @@ public class DefaultNamespaceManager implements NamespaceManager {
|
||||
|
||||
@Subscribe
|
||||
public void cleanupDeletedNamespaces(RepositoryEvent repositoryEvent) {
|
||||
HandlerEventType eventType = repositoryEvent.getEventType();
|
||||
if (eventType == HandlerEventType.DELETE || eventType == HandlerEventType.MODIFY && !repositoryEvent.getItem().getNamespace().equals(repositoryEvent.getOldItem().getNamespace())) {
|
||||
if (namespaceRelevantChange(repositoryEvent)) {
|
||||
Collection<String> allNamespaces = repositoryManager.getAllNamespaces();
|
||||
String oldNamespace = getOldNamespace(repositoryEvent);
|
||||
if (!allNamespaces.contains(oldNamespace)) {
|
||||
@@ -90,6 +89,12 @@ public class DefaultNamespaceManager implements NamespaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean namespaceRelevantChange(RepositoryEvent repositoryEvent) {
|
||||
HandlerEventType eventType = repositoryEvent.getEventType();
|
||||
return eventType == HandlerEventType.DELETE
|
||||
|| eventType == HandlerEventType.MODIFY && !repositoryEvent.getItem().getNamespace().equals(repositoryEvent.getOldItem().getNamespace());
|
||||
}
|
||||
|
||||
public String getOldNamespace(RepositoryEvent repositoryEvent) {
|
||||
if (repositoryEvent.getEventType() == HandlerEventType.DELETE) {
|
||||
return repositoryEvent.getItem().getNamespace();
|
||||
|
||||
@@ -162,7 +162,12 @@ public class AuthorizationChangedEventProducer {
|
||||
Repository repository = event.getItem();
|
||||
if (isAuthorizationDataModified(repository.getPermissions(), event.getItemBeforeModification().getPermissions())) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because a relevant field of repository {}/{} has changed", repository.getNamespace(), repository.getName()
|
||||
"fire authorization changed event, because the permissions of repository {}/{} have changed", repository.getNamespace(), repository.getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
} else if (!event.getItem().getNamespace().equals(event.getItemBeforeModification().getNamespace())) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because the namespace of repository {}/{} has changed", repository.getNamespace(), repository.getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
} else {
|
||||
|
||||
@@ -95,11 +95,11 @@
|
||||
},
|
||||
"namespace": {
|
||||
"permissionRead": {
|
||||
"displayName": "read permissions on namespaces",
|
||||
"displayName": "Read permissions on namespaces",
|
||||
"description": "May see the permissions set for namespaces"
|
||||
},
|
||||
"permissionWrite": {
|
||||
"displayName": "modify permissions on namespaces",
|
||||
"displayName": "Modify permissions on namespaces",
|
||||
"description": "May modify the permissions set for namespaces"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -62,5 +62,4 @@ class BranchToBranchDtoMapperTest {
|
||||
BranchDto dto = mapper.map(branch, new NamespaceAndName("hitchhiker", "heart-of-gold"));
|
||||
assertThat(dto.getLinks().getLinkBy("ka").get().getHref()).isEqualTo("http://hitchhiker/heart-of-gold/master");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -224,26 +224,6 @@ class NamespaceRootResourceTest {
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCreateNewPermission() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions")
|
||||
.content("{\"name\":\"dent\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":false}".getBytes())
|
||||
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotDeletePermission() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.delete("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithWritePermission {
|
||||
|
||||
|
||||
@@ -24,8 +24,13 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.github.legman.EventBus;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
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.Mock;
|
||||
@@ -41,6 +46,7 @@ import java.util.Optional;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.HandlerEventType.DELETE;
|
||||
@@ -54,6 +60,8 @@ class DefaultNamespaceManagerTest {
|
||||
RepositoryManager repositoryManager;
|
||||
@Mock
|
||||
ScmEventBus eventBus;
|
||||
@Mock
|
||||
Subject subject;
|
||||
|
||||
Namespace life;
|
||||
|
||||
@@ -64,8 +72,7 @@ class DefaultNamespaceManagerTest {
|
||||
|
||||
@BeforeEach
|
||||
void mockExistingNamespaces() {
|
||||
dao = new NamespaceDao(new InMemoryDataStoreFactory(new InMemoryDataStore()));
|
||||
manager = new DefaultNamespaceManager(repositoryManager, dao, eventBus);
|
||||
dao = new NamespaceDao(new InMemoryDataStoreFactory(new InMemoryDataStore<Namespace>()));
|
||||
|
||||
when(repositoryManager.getAllNamespaces()).thenReturn(asList("life", "universe", "rest"));
|
||||
|
||||
@@ -76,6 +83,18 @@ class DefaultNamespaceManagerTest {
|
||||
|
||||
universe = new Namespace("universe");
|
||||
rest = new Namespace("rest");
|
||||
|
||||
manager = new DefaultNamespaceManager(repositoryManager, dao, eventBus);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void mockSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,49 +104,6 @@ class DefaultNamespaceManagerTest {
|
||||
assertThat(namespace).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewNamespaceObjectIfNotInStore() {
|
||||
Namespace namespace = manager.get("universe").orElse(null);
|
||||
|
||||
assertThat(namespace).isEqualTo(universe);
|
||||
assertThat(namespace.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichExistingNamespaceWithPermissions() {
|
||||
Namespace namespace = manager.get("life").orElse(null);
|
||||
|
||||
assertThat(namespace.getPermissions()).containsExactly(life.getPermissions().toArray(new RepositoryPermission[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichExistingNamespaceWithPermissionsInGetAll() {
|
||||
Collection<Namespace> namespaces = manager.getAll();
|
||||
|
||||
assertThat(namespaces).containsExactly(
|
||||
life,
|
||||
universe,
|
||||
rest
|
||||
);
|
||||
Namespace foundLifeNamespace = namespaces.stream().filter(namespace -> namespace.getNamespace().equals("life")).findFirst().get();
|
||||
assertThat(
|
||||
foundLifeNamespace.getPermissions()).containsExactly(life.getPermissions().toArray(new RepositoryPermission[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldModifyExistingNamespaceWithPermissions() {
|
||||
Namespace modifiedNamespace = manager.get("life").get();
|
||||
|
||||
modifiedNamespace.setPermissions(asList(new RepositoryPermission("Arthur Dent", "READ", false)));
|
||||
manager.modify(modifiedNamespace);
|
||||
|
||||
Namespace newLife = manager.get("life").get();
|
||||
|
||||
assertThat(newLife).isEqualTo(modifiedNamespace);
|
||||
verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent)event).getEventType() == HandlerEventType.BEFORE_MODIFY));
|
||||
verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent)event).getEventType() == HandlerEventType.MODIFY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCleanUpPermissionWhenLastRepositoryOfNamespaceWasDeleted() {
|
||||
when(repositoryManager.getAllNamespaces()).thenReturn(asList("universe", "rest"));
|
||||
@@ -149,4 +125,93 @@ class DefaultNamespaceManagerTest {
|
||||
|
||||
assertThat(dao.get("life")).isEmpty();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithPermissionToReadPermissions {
|
||||
|
||||
@BeforeEach
|
||||
void grantReadPermission() {
|
||||
when(subject.isPermitted("namespace:permissionRead")).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewNamespaceObjectIfNotInStore() {
|
||||
Namespace namespace = manager.get("universe").orElse(null);
|
||||
|
||||
assertThat(namespace).isEqualTo(universe);
|
||||
assertThat(namespace.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichExistingNamespaceWithPermissions() {
|
||||
Namespace namespace = manager.get("life").orElse(null);
|
||||
|
||||
assertThat(namespace.getPermissions()).containsExactly(life.getPermissions().toArray(new RepositoryPermission[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichExistingNamespaceWithPermissionsInGetAll() {
|
||||
Collection<Namespace> namespaces = manager.getAll();
|
||||
|
||||
assertThat(namespaces).containsExactly(
|
||||
life,
|
||||
universe,
|
||||
rest
|
||||
);
|
||||
Namespace foundLifeNamespace = namespaces.stream().filter(namespace -> namespace.getNamespace().equals("life")).findFirst().get();
|
||||
assertThat(
|
||||
foundLifeNamespace.getPermissions()).containsExactly(life.getPermissions().toArray(new RepositoryPermission[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldModifyExistingNamespaceWithPermissions() {
|
||||
Namespace modifiedNamespace = manager.get("life").get();
|
||||
|
||||
modifiedNamespace.setPermissions(asList(new RepositoryPermission("Arthur Dent", "READ", false)));
|
||||
manager.modify(modifiedNamespace);
|
||||
|
||||
Namespace newLife = manager.get("life").get();
|
||||
|
||||
assertThat(newLife).isEqualTo(modifiedNamespace);
|
||||
verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent) event).getEventType() == HandlerEventType.BEFORE_MODIFY));
|
||||
verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent) event).getEventType() == HandlerEventType.MODIFY));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithoutPermissionToReadOrWritePermissions {
|
||||
|
||||
@BeforeEach
|
||||
void grantReadPermission() {
|
||||
when(subject.isPermitted("namespace:permissionRead")).thenReturn(false);
|
||||
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionWrite");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotEnrichExistingNamespaceWithPermissions() {
|
||||
Namespace namespace = manager.get("life").orElse(null);
|
||||
|
||||
assertThat(namespace.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotEnrichExistingNamespaceWithPermissionsInGetAll() {
|
||||
Collection<Namespace> namespaces = manager.getAll();
|
||||
|
||||
assertThat(namespaces).containsExactly(
|
||||
new Namespace("life"),
|
||||
universe,
|
||||
rest
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotModifyExistingNamespaceWithPermissions() {
|
||||
Namespace modifiedNamespace = manager.get("life").get();
|
||||
|
||||
modifiedNamespace.setPermissions(asList(new RepositoryPermission("Arthur Dent", "READ", false)));
|
||||
|
||||
Assertions.assertThrows(AuthorizationException.class, () -> manager.modify(modifiedNamespace));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,18 @@ public class AuthorizationChangedEventProducerTest {
|
||||
assertEventIsNotFired();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnRepositoryNamespaceChanged()
|
||||
{
|
||||
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
|
||||
repositoryModified.setName("test123");
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
repositoryModified.setNamespace("new_namespace");
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
private void resetStoredEvent(){
|
||||
producer.event = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user