mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 14:35:45 +01:00
Merged in feature/plugin_center (pull request #284)
Feature/plugin center
This commit is contained in:
@@ -37,6 +37,8 @@ public class VndMediaType {
|
|||||||
public static final String REPOSITORY_VERB_COLLECTION = PREFIX + "repositoryVerbCollection" + SUFFIX;
|
public static final String REPOSITORY_VERB_COLLECTION = PREFIX + "repositoryVerbCollection" + SUFFIX;
|
||||||
public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
|
public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
|
||||||
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
|
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
|
||||||
|
public static final String PLUGIN = PREFIX + "plugin" + SUFFIX;
|
||||||
|
public static final String PLUGIN_COLLECTION = PREFIX + "pluginCollection" + SUFFIX;
|
||||||
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
|
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
|
||||||
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
||||||
@SuppressWarnings("squid:S2068")
|
@SuppressWarnings("squid:S2068")
|
||||||
|
|||||||
87
scm-ui-components/packages/ui-components/src/CardColumn.js
Normal file
87
scm-ui-components/packages/ui-components/src/CardColumn.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
//@flow
|
||||||
|
import * as React from "react";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
inner: {
|
||||||
|
position: "relative",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: 1
|
||||||
|
},
|
||||||
|
innerLink: {
|
||||||
|
pointerEvents: "all"
|
||||||
|
},
|
||||||
|
centerImage: {
|
||||||
|
marginTop: "0.8em",
|
||||||
|
marginLeft: "1em !important"
|
||||||
|
},
|
||||||
|
flexFullHeight: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignSelf: "stretch"
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
display: "flex",
|
||||||
|
marginTop: "auto",
|
||||||
|
paddingBottom: "1.5rem"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
avatar: React.Node,
|
||||||
|
footerLeft: React.Node,
|
||||||
|
footerRight: React.Node,
|
||||||
|
link: string,
|
||||||
|
// context props
|
||||||
|
classes: any
|
||||||
|
};
|
||||||
|
|
||||||
|
class CardColumn extends React.Component<Props> {
|
||||||
|
createLink = () => {
|
||||||
|
const { link } = this.props;
|
||||||
|
if (link) {
|
||||||
|
return <Link className="overlay-column" to={link} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { avatar, title, description, footerLeft, footerRight, classes } = this.props;
|
||||||
|
const link = this.createLink();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{link}
|
||||||
|
<article className={classNames("media", classes.inner)}>
|
||||||
|
<figure className={classNames(classes.centerImage, "media-left")}>
|
||||||
|
{avatar}
|
||||||
|
</figure>
|
||||||
|
<div className={classNames("media-content", "text-box", classes.flexFullHeight)}>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<div className="content shorten-text">
|
||||||
|
<p className="is-marginless">
|
||||||
|
<strong>{title}</strong>
|
||||||
|
</p>
|
||||||
|
<p className="shorten-text">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(classes.footer, "level")}>
|
||||||
|
<div className="level-left is-hidden-mobile">{footerLeft}</div>
|
||||||
|
<div className="level-right is-mobile">{footerRight}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectSheet(styles)(CardColumn);
|
||||||
103
scm-ui-components/packages/ui-components/src/CardColumnGroup.js
Normal file
103
scm-ui-components/packages/ui-components/src/CardColumnGroup.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
//@flow
|
||||||
|
import * as React from "react";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
pointer: {
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "1.5rem"
|
||||||
|
},
|
||||||
|
repoGroup: {
|
||||||
|
marginBottom: "1em"
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
padding: "0 0.75rem"
|
||||||
|
},
|
||||||
|
clearfix: {
|
||||||
|
clear: "both"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string,
|
||||||
|
elements: React.Node[],
|
||||||
|
|
||||||
|
// context props
|
||||||
|
classes: any
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
collapsed: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class CardColumnGroup extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
collapsed: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCollapse = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
collapsed: !prevState.collapsed
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
isLastEntry = (array: React.Node[], index: number) => {
|
||||||
|
return index === array.length - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
isLengthOdd = (array: React.Node[]) => {
|
||||||
|
return array.length % 2 !== 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
isFullSize = (array: React.Node[], index: number) => {
|
||||||
|
return this.isLastEntry(array, index) && this.isLengthOdd(array);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, elements, classes } = this.props;
|
||||||
|
const { collapsed } = this.state;
|
||||||
|
|
||||||
|
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
|
||||||
|
let content = null;
|
||||||
|
if (!collapsed) {
|
||||||
|
content = elements.map((entry, index) => {
|
||||||
|
const fullColumnWidth = this.isFullSize(elements, index);
|
||||||
|
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"box",
|
||||||
|
"box-link-shadow",
|
||||||
|
"column",
|
||||||
|
"is-clipped",
|
||||||
|
sizeClass
|
||||||
|
)}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{entry}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classes.repoGroup}>
|
||||||
|
<h2>
|
||||||
|
<span className={classes.pointer} onClick={this.toggleCollapse}>
|
||||||
|
<i className={classNames("fa", icon)} /> {name}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<hr />
|
||||||
|
<div className={classNames("columns", "is-multiline", classes.wrapper)}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
<div className={classes.clearfix} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectSheet(styles)(CardColumnGroup);
|
||||||
@@ -25,7 +25,7 @@ class LinkPaginator extends React.Component<Props> {
|
|||||||
renderFirstButton() {
|
renderFirstButton() {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={"pagination-link"}
|
className="pagination-link"
|
||||||
label={"1"}
|
label={"1"}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
link={this.addFilterToLink("1")}
|
link={this.addFilterToLink("1")}
|
||||||
@@ -69,7 +69,7 @@ class LinkPaginator extends React.Component<Props> {
|
|||||||
const { collection } = this.props;
|
const { collection } = this.props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={"pagination-link"}
|
className="pagination-link"
|
||||||
label={`${collection.pageTotal}`}
|
label={`${collection.pageTotal}`}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
link={this.addFilterToLink(`${collection.pageTotal}`)}
|
link={this.addFilterToLink(`${collection.pageTotal}`)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class StatePaginator extends React.Component<Props> {
|
|||||||
renderFirstButton() {
|
renderFirstButton() {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={"pagination-link"}
|
className="pagination-link"
|
||||||
label={"1"}
|
label={"1"}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
action={() => this.updateCurrentPage(1)}
|
action={() => this.updateCurrentPage(1)}
|
||||||
@@ -35,7 +35,7 @@ class StatePaginator extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={"pagination-previous"}
|
className="pagination-previous"
|
||||||
label={label ? label : previousPage.toString()}
|
label={label ? label : previousPage.toString()}
|
||||||
disabled={!this.hasLink("prev")}
|
disabled={!this.hasLink("prev")}
|
||||||
action={() => this.updateCurrentPage(previousPage)}
|
action={() => this.updateCurrentPage(previousPage)}
|
||||||
@@ -53,7 +53,7 @@ class StatePaginator extends React.Component<Props> {
|
|||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={"pagination-next"}
|
className="pagination-next"
|
||||||
label={label ? label : nextPage.toString()}
|
label={label ? label : nextPage.toString()}
|
||||||
disabled={!this.hasLink("next")}
|
disabled={!this.hasLink("next")}
|
||||||
action={() => this.updateCurrentPage(nextPage)}
|
action={() => this.updateCurrentPage(nextPage)}
|
||||||
@@ -65,7 +65,7 @@ class StatePaginator extends React.Component<Props> {
|
|||||||
const { collection } = this.props;
|
const { collection } = this.props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={"pagination-link"}
|
className="pagination-link"
|
||||||
label={`${collection.pageTotal}`}
|
label={`${collection.pageTotal}`}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
action={() => this.updateCurrentPage(collection.pageTotal)}
|
action={() => this.updateCurrentPage(collection.pageTotal)}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export { default as MarkdownView } from "./MarkdownView";
|
|||||||
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
|
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
|
||||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||||
export { default as OverviewPageActions } from "./OverviewPageActions.js";
|
export { default as OverviewPageActions } from "./OverviewPageActions.js";
|
||||||
|
export { default as CardColumnGroup } from "./CardColumnGroup";
|
||||||
|
export { default as CardColumn } from "./CardColumn";
|
||||||
|
|
||||||
export { apiClient } from "./apiclient.js";
|
export { apiClient } from "./apiclient.js";
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
|
|||||||
22
scm-ui-components/packages/ui-types/src/Plugin.js
Normal file
22
scm-ui-components/packages/ui-types/src/Plugin.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//@flow
|
||||||
|
import type { Collection, Links } from "./hal";
|
||||||
|
|
||||||
|
export type Plugin = {
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
version: string,
|
||||||
|
author: string,
|
||||||
|
description?: string,
|
||||||
|
_links: Links
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginCollection = Collection & {
|
||||||
|
_embedded: {
|
||||||
|
plugins: Plugin[] | string[]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginGroup = {
|
||||||
|
name: string,
|
||||||
|
plugins: Plugin[]
|
||||||
|
};
|
||||||
@@ -25,6 +25,8 @@ export type { SubRepository, File } from "./Sources";
|
|||||||
|
|
||||||
export type { SelectValue, AutocompleteObject } from "./Autocomplete";
|
export type { SelectValue, AutocompleteObject } from "./Autocomplete";
|
||||||
|
|
||||||
|
export type { Plugin, PluginCollection, PluginGroup } from "./Plugin";
|
||||||
|
|
||||||
export type { RepositoryRole } from "./RepositoryRole";
|
export type { RepositoryRole } from "./RepositoryRole";
|
||||||
|
|
||||||
export type { NamespaceStrategies } from "./NamespaceStrategies";
|
export type { NamespaceStrategies } from "./NamespaceStrategies";
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"currentAppVersion": "Aktuelle Software-Versionsnummer"
|
"currentAppVersion": "Aktuelle Software-Versionsnummer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugins": {
|
||||||
|
"title": "Plugins",
|
||||||
|
"installedSubtitle": "Installierte Plugins",
|
||||||
|
"availableSubtitle": "Verfügbare Plugins",
|
||||||
|
"menu": {
|
||||||
|
"pluginsNavLink": "Plugins",
|
||||||
|
"installedNavLink": "Installiert",
|
||||||
|
"availableNavLink": "Verfügbar"
|
||||||
|
},
|
||||||
|
"noPlugins": "Keine Plugins gefunden."
|
||||||
|
},
|
||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
"navLink": "Berechtigungsrollen",
|
"navLink": "Berechtigungsrollen",
|
||||||
"title": "Berechtigungsrollen",
|
"title": "Berechtigungsrollen",
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"currentAppVersion": "Current Application Version"
|
"currentAppVersion": "Current Application Version"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugins": {
|
||||||
|
"title": "Plugins",
|
||||||
|
"installedSubtitle": "Installed Plugins",
|
||||||
|
"availableSubtitle": "Available Plugins",
|
||||||
|
"menu": {
|
||||||
|
"pluginsNavLink": "Plugins",
|
||||||
|
"installedNavLink": "Installed",
|
||||||
|
"availableNavLink": "Available"
|
||||||
|
},
|
||||||
|
"noPlugins": "No plugins found."
|
||||||
|
},
|
||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
"navLink": "Permission Roles",
|
"navLink": "Permission Roles",
|
||||||
"title": "Permission Roles",
|
"title": "Permission Roles",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { Links } from "@scm-manager/ui-types";
|
|||||||
import { Page, Navigation, NavLink, Section, SubNavigation } from "@scm-manager/ui-components";
|
import { Page, Navigation, NavLink, Section, SubNavigation } from "@scm-manager/ui-components";
|
||||||
import { getLinks } from "../../modules/indexResource";
|
import { getLinks } from "../../modules/indexResource";
|
||||||
import AdminDetails from "./AdminDetails";
|
import AdminDetails from "./AdminDetails";
|
||||||
|
import PluginsOverview from "../plugins/containers/PluginsOverview";
|
||||||
import GlobalConfig from "./GlobalConfig";
|
import GlobalConfig from "./GlobalConfig";
|
||||||
import RepositoryRoles from "../roles/containers/RepositoryRoles";
|
import RepositoryRoles from "../roles/containers/RepositoryRoles";
|
||||||
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
|
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
|
||||||
@@ -62,6 +63,35 @@ class Admin extends React.Component<Props> {
|
|||||||
<Redirect exact from={url} to={`${url}/info`} />
|
<Redirect exact from={url} to={`${url}/info`} />
|
||||||
<Route path={`${url}/info`} exact component={AdminDetails} />
|
<Route path={`${url}/info`} exact component={AdminDetails} />
|
||||||
<Route path={`${url}/settings/general`} exact component={GlobalConfig} />
|
<Route path={`${url}/settings/general`} exact component={GlobalConfig} />
|
||||||
|
<Redirect exact from={`${url}/plugins`} to={`${url}/plugins/installed/`} />
|
||||||
|
<Route
|
||||||
|
path={`${url}/plugins/installed`}
|
||||||
|
exact
|
||||||
|
render={() => (
|
||||||
|
<PluginsOverview baseUrl={`${url}/plugins/installed`} installed={true} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${url}/plugins/installed/:page`}
|
||||||
|
exact
|
||||||
|
render={() => (
|
||||||
|
<PluginsOverview baseUrl={`${url}/plugins/installed`} installed={true} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${url}/plugins/available`}
|
||||||
|
exact
|
||||||
|
render={() => (
|
||||||
|
<PluginsOverview baseUrl={`${url}/plugins/available`} installed={false} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${url}/plugins/available/:page`}
|
||||||
|
exact
|
||||||
|
render={() => (
|
||||||
|
<PluginsOverview baseUrl={`${url}/plugins/available`} installed={false} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${url}/role/:role`}
|
path={`${url}/role/:role`}
|
||||||
render={() => (
|
render={() => (
|
||||||
@@ -106,6 +136,24 @@ class Admin extends React.Component<Props> {
|
|||||||
icon="fas fa-info-circle"
|
icon="fas fa-info-circle"
|
||||||
label={t("admin.menu.informationNavLink")}
|
label={t("admin.menu.informationNavLink")}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
links.plugins &&
|
||||||
|
<SubNavigation
|
||||||
|
to={`${url}/plugins/`}
|
||||||
|
icon="fas fa-puzzle-piece"
|
||||||
|
label={t("plugins.menu.pluginsNavLink")}
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
to={`${url}/plugins/installed/`}
|
||||||
|
label={t("plugins.menu.installedNavLink")}
|
||||||
|
/>
|
||||||
|
{/* Activate this again after available plugins page is created */}
|
||||||
|
{/*<NavLink*/}
|
||||||
|
{/* to={`${url}/plugins/available/`}*/}
|
||||||
|
{/* label={t("plugins.menu.availableNavLink")}*/}
|
||||||
|
{/*/>*/}
|
||||||
|
</SubNavigation>
|
||||||
|
}
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`${url}/roles/`}
|
to={`${url}/roles/`}
|
||||||
icon="fas fa-user-shield"
|
icon="fas fa-user-shield"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import {Loading, Subtitle} from "@scm-manager/ui-components";
|
import { Loading, Title, Subtitle } from "@scm-manager/ui-components";
|
||||||
import {getAppVersion} from "../../modules/indexResource";
|
import { getAppVersion } from "../../modules/indexResource";
|
||||||
import {connect} from "react-redux";
|
|
||||||
import Title from "@scm-manager/ui-components/src/layout/Title";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
@@ -24,10 +23,12 @@ class AdminDetails extends React.Component<Props> {
|
|||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<Title title={t("admin.information.currentAppVersion")}/>
|
<>
|
||||||
<Subtitle subtitle={this.props.version}/>
|
<Title title={t("admin.information.currentAppVersion")} />
|
||||||
</>;
|
<Subtitle subtitle={this.props.version} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
scm-ui/src/admin/plugins/components/PluginAvatar.js
Normal file
22
scm-ui/src/admin/plugins/components/PluginAvatar.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
|
import type { Plugin } from "@scm-manager/ui-types";
|
||||||
|
import { Image } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
plugin: Plugin
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class PluginAvatar extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { plugin } = this.props;
|
||||||
|
return (
|
||||||
|
<p className="image is-64x64">
|
||||||
|
<ExtensionPoint name="plugins.plugin-avatar" props={{ plugin }}>
|
||||||
|
<Image src="/images/blib.jpg" alt="Logo" />
|
||||||
|
</ExtensionPoint>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
scm-ui/src/admin/plugins/components/PluginEntry.js
Normal file
44
scm-ui/src/admin/plugins/components/PluginEntry.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Plugin } from "@scm-manager/ui-types";
|
||||||
|
import { CardColumn } from "@scm-manager/ui-components";
|
||||||
|
import PluginAvatar from "./PluginAvatar";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
plugin: Plugin
|
||||||
|
};
|
||||||
|
|
||||||
|
class PluginEntry extends React.Component<Props> {
|
||||||
|
createAvatar = (plugin: Plugin) => {
|
||||||
|
return <PluginAvatar plugin={plugin} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
createFooterLeft = (plugin: Plugin) => {
|
||||||
|
return <small className="level-item">{plugin.author}</small>;
|
||||||
|
};
|
||||||
|
|
||||||
|
createFooterRight = (plugin: Plugin) => {
|
||||||
|
return <p className="level-item">{plugin.version}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { plugin } = this.props;
|
||||||
|
const avatar = this.createAvatar(plugin);
|
||||||
|
const footerLeft = this.createFooterLeft(plugin);
|
||||||
|
const footerRight = this.createFooterRight(plugin);
|
||||||
|
|
||||||
|
// TODO: Add link to plugin page below
|
||||||
|
return (
|
||||||
|
<CardColumn
|
||||||
|
link="#"
|
||||||
|
avatar={avatar}
|
||||||
|
title={plugin.name}
|
||||||
|
description={plugin.description}
|
||||||
|
footerLeft={footerLeft}
|
||||||
|
footerRight={footerRight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginEntry;
|
||||||
21
scm-ui/src/admin/plugins/components/PluginGroupEntry.js
Normal file
21
scm-ui/src/admin/plugins/components/PluginGroupEntry.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { CardColumnGroup } from "@scm-manager/ui-components";
|
||||||
|
import type { PluginGroup } from "@scm-manager/ui-types";
|
||||||
|
import PluginEntry from "./PluginEntry";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
group: PluginGroup
|
||||||
|
};
|
||||||
|
|
||||||
|
class PluginGroupEntry extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { group } = this.props;
|
||||||
|
const entries = group.plugins.map((plugin, index) => {
|
||||||
|
return <PluginEntry plugin={plugin} key={index} />;
|
||||||
|
});
|
||||||
|
return <CardColumnGroup name={group.name} elements={entries} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginGroupEntry;
|
||||||
26
scm-ui/src/admin/plugins/components/PluginsList.js
Normal file
26
scm-ui/src/admin/plugins/components/PluginsList.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Plugin } from "@scm-manager/ui-types";
|
||||||
|
import PluginGroupEntry from "../components/PluginGroupEntry";
|
||||||
|
import groupByCategory from "./groupByCategory";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
plugins: Plugin[]
|
||||||
|
};
|
||||||
|
|
||||||
|
class PluginList extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { plugins } = this.props;
|
||||||
|
|
||||||
|
const groups = groupByCategory(plugins);
|
||||||
|
return (
|
||||||
|
<div className="content is-plugin-page">
|
||||||
|
{groups.map(group => {
|
||||||
|
return <PluginGroupEntry group={group} key={group.name} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginList;
|
||||||
39
scm-ui/src/admin/plugins/components/groupByCategory.js
Normal file
39
scm-ui/src/admin/plugins/components/groupByCategory.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// @flow
|
||||||
|
import type { Plugin, PluginGroup } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
export default function groupByCategory(
|
||||||
|
plugins: Plugin[]
|
||||||
|
): PluginGroup[] {
|
||||||
|
let groups = {};
|
||||||
|
for (let plugin of plugins) {
|
||||||
|
const groupName = plugin.type;
|
||||||
|
|
||||||
|
let group = groups[groupName];
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
name: groupName,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
|
groups[groupName] = group;
|
||||||
|
}
|
||||||
|
group.plugins.push(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupArray = [];
|
||||||
|
for (let groupName in groups) {
|
||||||
|
const group = groups[groupName];
|
||||||
|
group.plugins.sort(sortByName);
|
||||||
|
groupArray.push(groups[groupName]);
|
||||||
|
}
|
||||||
|
groupArray.sort(sortByName);
|
||||||
|
return groupArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByName(a, b) {
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
108
scm-ui/src/admin/plugins/containers/PluginsOverview.js
Normal file
108
scm-ui/src/admin/plugins/containers/PluginsOverview.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { compose } from "redux";
|
||||||
|
import type { PluginCollection } from "@scm-manager/ui-types";
|
||||||
|
import {
|
||||||
|
Loading,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Notification,
|
||||||
|
ErrorNotification
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
|
import {
|
||||||
|
fetchPluginsByLink,
|
||||||
|
getFetchPluginsFailure,
|
||||||
|
getPluginCollection,
|
||||||
|
isFetchPluginsPending
|
||||||
|
} from "../modules/plugins";
|
||||||
|
import PluginsList from "../components/PluginsList";
|
||||||
|
import { getPluginsLink } from "../../../modules/indexResource";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
|
collection: PluginCollection,
|
||||||
|
baseUrl: string,
|
||||||
|
installed: boolean,
|
||||||
|
pluginsLink: string,
|
||||||
|
|
||||||
|
// context objects
|
||||||
|
t: string => string,
|
||||||
|
|
||||||
|
// dispatched functions
|
||||||
|
fetchPluginsByLink: (link: string) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
class PluginsOverview extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { fetchPluginsByLink, pluginsLink } = this.props;
|
||||||
|
fetchPluginsByLink(pluginsLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, error, collection, installed, t } = this.props;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorNotification error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collection || loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title title={t("plugins.title")} />
|
||||||
|
<Subtitle
|
||||||
|
subtitle={
|
||||||
|
installed
|
||||||
|
? t("plugins.installedSubtitle")
|
||||||
|
: t("plugins.availableSubtitle")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{this.renderPluginsList()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPluginsList() {
|
||||||
|
const { collection, t } = this.props;
|
||||||
|
|
||||||
|
if (collection._embedded && collection._embedded.plugins.length > 0) {
|
||||||
|
return <PluginsList plugins={collection._embedded.plugins} />;
|
||||||
|
}
|
||||||
|
return <Notification type="info">{t("plugins.noPlugins")}</Notification>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const collection = getPluginCollection(state);
|
||||||
|
const loading = isFetchPluginsPending(state);
|
||||||
|
const error = getFetchPluginsFailure(state);
|
||||||
|
const pluginsLink = getPluginsLink(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
collection,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
pluginsLink
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
fetchPluginsByLink: (link: string) => {
|
||||||
|
dispatch(fetchPluginsByLink(link));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
translate("admin"),
|
||||||
|
connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)
|
||||||
|
)(PluginsOverview);
|
||||||
191
scm-ui/src/admin/plugins/modules/plugins.js
Normal file
191
scm-ui/src/admin/plugins/modules/plugins.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
// @flow
|
||||||
|
import * as types from "../../../modules/types";
|
||||||
|
import { isPending } from "../../../modules/pending";
|
||||||
|
import { getFailure } from "../../../modules/failure";
|
||||||
|
import type { Action, Plugin, PluginCollection } from "@scm-manager/ui-types";
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
export const FETCH_PLUGINS = "scm/plugins/FETCH_PLUGINS";
|
||||||
|
export const FETCH_PLUGINS_PENDING = `${FETCH_PLUGINS}_${types.PENDING_SUFFIX}`;
|
||||||
|
export const FETCH_PLUGINS_SUCCESS = `${FETCH_PLUGINS}_${types.SUCCESS_SUFFIX}`;
|
||||||
|
export const FETCH_PLUGINS_FAILURE = `${FETCH_PLUGINS}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
|
export const FETCH_PLUGIN = "scm/plugins/FETCH_PLUGIN";
|
||||||
|
export const FETCH_PLUGIN_PENDING = `${FETCH_PLUGIN}_${types.PENDING_SUFFIX}`;
|
||||||
|
export const FETCH_PLUGIN_SUCCESS = `${FETCH_PLUGIN}_${types.SUCCESS_SUFFIX}`;
|
||||||
|
export const FETCH_PLUGIN_FAILURE = `${FETCH_PLUGIN}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
|
// fetch plugins
|
||||||
|
export function fetchPluginsByLink(link: string) {
|
||||||
|
return function(dispatch: any) {
|
||||||
|
dispatch(fetchPluginsPending());
|
||||||
|
return apiClient
|
||||||
|
.get(link)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(plugins => {
|
||||||
|
dispatch(fetchPluginsSuccess(plugins));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(fetchPluginsFailure(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginsPending(): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_PLUGINS_PENDING
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginsSuccess(plugins: PluginCollection): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_PLUGINS_SUCCESS,
|
||||||
|
payload: plugins
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginsFailure(err: Error): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_PLUGINS_FAILURE,
|
||||||
|
payload: err
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch plugin
|
||||||
|
export function fetchPluginByLink(plugin: Plugin) {
|
||||||
|
return fetchPlugin(plugin._links.self.href, plugin.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginByName(link: string, name: string) {
|
||||||
|
const pluginUrl = link.endsWith("/") ? link : link + "/";
|
||||||
|
return fetchPlugin(pluginUrl + name, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPlugin(link: string, name: string) {
|
||||||
|
return function(dispatch: any) {
|
||||||
|
dispatch(fetchPluginPending(name));
|
||||||
|
return apiClient
|
||||||
|
.get(link)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(plugin => {
|
||||||
|
dispatch(fetchPluginSuccess(plugin));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(fetchPluginFailure(name, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginPending(name: string): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_PLUGIN_PENDING,
|
||||||
|
payload: {
|
||||||
|
name
|
||||||
|
},
|
||||||
|
itemId: name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginSuccess(plugin: Plugin): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_PLUGIN_SUCCESS,
|
||||||
|
payload: plugin,
|
||||||
|
itemId: plugin.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPluginFailure(name: string, error: Error): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_PLUGIN_FAILURE,
|
||||||
|
payload: {
|
||||||
|
name,
|
||||||
|
error
|
||||||
|
},
|
||||||
|
itemId: name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// reducer
|
||||||
|
function normalizeByName(pluginCollection: PluginCollection) {
|
||||||
|
const names = [];
|
||||||
|
const byNames = {};
|
||||||
|
for (const plugin of pluginCollection._embedded.plugins) {
|
||||||
|
names.push(plugin.name);
|
||||||
|
byNames[plugin.name] = plugin;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
list: {
|
||||||
|
...pluginCollection,
|
||||||
|
_embedded: {
|
||||||
|
plugins: names
|
||||||
|
}
|
||||||
|
},
|
||||||
|
byNames: byNames
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducerByNames = (state: Object, plugin: Plugin) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
byNames: {
|
||||||
|
...state.byNames,
|
||||||
|
[plugin.name]: plugin
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function reducer(
|
||||||
|
state: Object = {},
|
||||||
|
action: Action = { type: "UNKNOWN" }
|
||||||
|
): Object {
|
||||||
|
if (!action.payload) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case FETCH_PLUGINS_SUCCESS:
|
||||||
|
return normalizeByName(action.payload);
|
||||||
|
case FETCH_PLUGIN_SUCCESS:
|
||||||
|
return reducerByNames(state, action.payload);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
export function getPluginCollection(state: Object) {
|
||||||
|
if (state.plugins && state.plugins.list && state.plugins.byNames) {
|
||||||
|
const plugins = [];
|
||||||
|
for (let pluginName of state.plugins.list._embedded.plugins) {
|
||||||
|
plugins.push(state.plugins.byNames[pluginName]);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state.plugins.list,
|
||||||
|
_embedded: {
|
||||||
|
plugins
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFetchPluginsPending(state: Object) {
|
||||||
|
return isPending(state, FETCH_PLUGINS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchPluginsFailure(state: Object) {
|
||||||
|
return getFailure(state, FETCH_PLUGINS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlugin(state: Object, name: string) {
|
||||||
|
if (state.plugins && state.plugins.byNames) {
|
||||||
|
return state.plugins.byNames[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFetchPluginPending(state: Object, name: string) {
|
||||||
|
return isPending(state, FETCH_PLUGIN, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchPluginFailure(state: Object, name: string) {
|
||||||
|
return getFailure(state, FETCH_PLUGIN, name);
|
||||||
|
}
|
||||||
353
scm-ui/src/admin/plugins/modules/plugins.test.js
Normal file
353
scm-ui/src/admin/plugins/modules/plugins.test.js
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
// @flow
|
||||||
|
import configureMockStore from "redux-mock-store";
|
||||||
|
import thunk from "redux-thunk";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import reducer, {
|
||||||
|
FETCH_PLUGINS,
|
||||||
|
FETCH_PLUGINS_PENDING,
|
||||||
|
FETCH_PLUGINS_SUCCESS,
|
||||||
|
FETCH_PLUGINS_FAILURE,
|
||||||
|
FETCH_PLUGIN,
|
||||||
|
FETCH_PLUGIN_PENDING,
|
||||||
|
FETCH_PLUGIN_SUCCESS,
|
||||||
|
FETCH_PLUGIN_FAILURE,
|
||||||
|
fetchPluginsByLink,
|
||||||
|
fetchPluginsSuccess,
|
||||||
|
getPluginCollection,
|
||||||
|
isFetchPluginsPending,
|
||||||
|
getFetchPluginsFailure,
|
||||||
|
fetchPluginByLink,
|
||||||
|
fetchPluginByName,
|
||||||
|
fetchPluginSuccess,
|
||||||
|
getPlugin,
|
||||||
|
isFetchPluginPending,
|
||||||
|
getFetchPluginFailure
|
||||||
|
} from "./plugins";
|
||||||
|
import type { Plugin, PluginCollection } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
const groupManagerPlugin: Plugin = {
|
||||||
|
name: "scm-groupmanager-plugin",
|
||||||
|
bundles: ["/scm/groupmanager-plugin.bundle.js"],
|
||||||
|
type: "Administration",
|
||||||
|
version: "2.0.0-SNAPSHOT",
|
||||||
|
author: "Sebastian Sdorra",
|
||||||
|
description: "Notify a remote webserver whenever a plugin is pushed to.",
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scriptPlugin: Plugin = {
|
||||||
|
name: "scm-script-plugin",
|
||||||
|
bundles: ["/scm/script-plugin.bundle.js"],
|
||||||
|
type: "Miscellaneous",
|
||||||
|
version: "2.0.0-SNAPSHOT",
|
||||||
|
author: "Sebastian Sdorra",
|
||||||
|
description: "Script support for scm-manager.",
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/api/v2/ui/plugins/scm-script-plugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const branchwpPlugin: Plugin = {
|
||||||
|
name: "scm-branchwp-plugin",
|
||||||
|
bundles: ["/scm/branchwp-plugin.bundle.js"],
|
||||||
|
type: "Miscellaneous",
|
||||||
|
version: "2.0.0-SNAPSHOT",
|
||||||
|
author: "Sebastian Sdorra",
|
||||||
|
description: "This plugin adds branch write protection for plugins.",
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/api/v2/ui/plugins/scm-branchwp-plugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pluginCollectionWithNames: PluginCollection = {
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/api/v2/ui/plugins"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_embedded: {
|
||||||
|
plugins: [groupManagerPlugin.name, scriptPlugin.name, branchwpPlugin.name]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pluginCollection: PluginCollection = {
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: "http://localhost:8081/api/v2/ui/plugins"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_embedded: {
|
||||||
|
plugins: [groupManagerPlugin, scriptPlugin, branchwpPlugin]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("plugins fetch", () => {
|
||||||
|
const URL = "ui/plugins";
|
||||||
|
const PLUGINS_URL = "/api/v2/ui/plugins";
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch plugins from link", () => {
|
||||||
|
fetchMock.getOnce(PLUGINS_URL, pluginCollection);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_PLUGINS_PENDING },
|
||||||
|
{
|
||||||
|
type: FETCH_PLUGINS_SUCCESS,
|
||||||
|
payload: pluginCollection
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchPluginsByLink(URL)).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch FETCH_PLUGINS_FAILURE if request fails", () => {
|
||||||
|
fetchMock.getOnce(PLUGINS_URL, {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchPluginsByLink(URL)).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_PLUGINS_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_PLUGINS_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch scm-groupmanager-plugin by name", () => {
|
||||||
|
fetchMock.getOnce(
|
||||||
|
PLUGINS_URL + "/scm-groupmanager-plugin",
|
||||||
|
groupManagerPlugin
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: FETCH_PLUGIN_PENDING,
|
||||||
|
payload: {
|
||||||
|
name: "scm-groupmanager-plugin"
|
||||||
|
},
|
||||||
|
itemId: "scm-groupmanager-plugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FETCH_PLUGIN_SUCCESS,
|
||||||
|
payload: groupManagerPlugin,
|
||||||
|
itemId: "scm-groupmanager-plugin"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store
|
||||||
|
.dispatch(fetchPluginByName(URL, "scm-groupmanager-plugin"))
|
||||||
|
.then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch FETCH_PLUGIN_FAILURE, if the request for scm-groupmanager-plugin by name fails", () => {
|
||||||
|
fetchMock.getOnce(PLUGINS_URL + "/scm-groupmanager-plugin", {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store
|
||||||
|
.dispatch(fetchPluginByName(URL, "scm-groupmanager-plugin"))
|
||||||
|
.then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_PLUGIN_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_PLUGIN_FAILURE);
|
||||||
|
expect(actions[1].payload.name).toBe("scm-groupmanager-plugin");
|
||||||
|
expect(actions[1].payload.error).toBeDefined();
|
||||||
|
expect(actions[1].itemId).toBe("scm-groupmanager-plugin");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch scm-groupmanager-plugin", () => {
|
||||||
|
fetchMock.getOnce(
|
||||||
|
"http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin",
|
||||||
|
groupManagerPlugin
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: FETCH_PLUGIN_PENDING,
|
||||||
|
payload: {
|
||||||
|
name: "scm-groupmanager-plugin"
|
||||||
|
},
|
||||||
|
itemId: "scm-groupmanager-plugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FETCH_PLUGIN_SUCCESS,
|
||||||
|
payload: groupManagerPlugin,
|
||||||
|
itemId: "scm-groupmanager-plugin"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchPluginByLink(groupManagerPlugin)).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch FETCH_PLUGIN_FAILURE, it the request for scm-groupmanager-plugin fails", () => {
|
||||||
|
fetchMock.getOnce(
|
||||||
|
"http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin",
|
||||||
|
{
|
||||||
|
status: 500
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchPluginByLink(groupManagerPlugin)).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_PLUGIN_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_PLUGIN_FAILURE);
|
||||||
|
expect(actions[1].payload.name).toBe("scm-groupmanager-plugin");
|
||||||
|
expect(actions[1].payload.error).toBeDefined();
|
||||||
|
expect(actions[1].itemId).toBe("scm-groupmanager-plugin");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("plugins reducer", () => {
|
||||||
|
it("should return empty object, if state and action is undefined", () => {
|
||||||
|
expect(reducer()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same state, if the action is undefined", () => {
|
||||||
|
const state = { x: true };
|
||||||
|
expect(reducer(state)).toBe(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same state, if the action is unknown to the reducer", () => {
|
||||||
|
const state = { x: true };
|
||||||
|
expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store the plugins by it's type and name on FETCH_PLUGINS_SUCCESS", () => {
|
||||||
|
const newState = reducer({}, fetchPluginsSuccess(pluginCollection));
|
||||||
|
expect(newState.list._embedded.plugins).toEqual([
|
||||||
|
"scm-groupmanager-plugin",
|
||||||
|
"scm-script-plugin",
|
||||||
|
"scm-branchwp-plugin"
|
||||||
|
]);
|
||||||
|
expect(newState.byNames["scm-groupmanager-plugin"]).toBe(
|
||||||
|
groupManagerPlugin
|
||||||
|
);
|
||||||
|
expect(newState.byNames["scm-script-plugin"]).toBe(scriptPlugin);
|
||||||
|
expect(newState.byNames["scm-branchwp-plugin"]).toBe(branchwpPlugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store the plugin at byNames", () => {
|
||||||
|
const newState = reducer({}, fetchPluginSuccess(groupManagerPlugin));
|
||||||
|
expect(newState.byNames["scm-groupmanager-plugin"]).toBe(
|
||||||
|
groupManagerPlugin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("plugins selectors", () => {
|
||||||
|
const error = new Error("something went wrong");
|
||||||
|
|
||||||
|
it("should return the plugins collection", () => {
|
||||||
|
const state = {
|
||||||
|
plugins: {
|
||||||
|
list: pluginCollectionWithNames,
|
||||||
|
byNames: {
|
||||||
|
"scm-groupmanager-plugin": groupManagerPlugin,
|
||||||
|
"scm-script-plugin": scriptPlugin,
|
||||||
|
"scm-branchwp-plugin": branchwpPlugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collection = getPluginCollection(state);
|
||||||
|
expect(collection).toEqual(pluginCollection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true, when fetch plugins is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[FETCH_PLUGINS]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isFetchPluginsPending(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when fetch plugins is not pending", () => {
|
||||||
|
expect(isFetchPluginsPending({})).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return error when fetch plugins did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[FETCH_PLUGINS]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getFetchPluginsFailure(state)).toEqual(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when fetch plugins did not fail", () => {
|
||||||
|
expect(getFetchPluginsFailure({})).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the plugin collection", () => {
|
||||||
|
const state = {
|
||||||
|
plugins: {
|
||||||
|
byNames: {
|
||||||
|
"scm-groupmanager-plugin": groupManagerPlugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const plugin = getPlugin(state, "scm-groupmanager-plugin");
|
||||||
|
expect(plugin).toEqual(groupManagerPlugin);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true, when fetch plugin is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[FETCH_PLUGIN + "/scm-groupmanager-plugin"]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isFetchPluginPending(state, "scm-groupmanager-plugin")).toEqual(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when fetch plugin is not pending", () => {
|
||||||
|
expect(isFetchPluginPending({}, "scm-groupmanager-plugin")).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return error when fetch plugin did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[FETCH_PLUGIN + "/scm-groupmanager-plugin"]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getFetchPluginFailure(state, "scm-groupmanager-plugin")).toEqual(
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when fetch plugin did not fail", () => {
|
||||||
|
expect(getFetchPluginFailure({}, "scm-groupmanager-plugin")).toBe(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ import config from "./admin/modules/config";
|
|||||||
import roles from "./admin/roles/modules/roles";
|
import roles from "./admin/roles/modules/roles";
|
||||||
import namespaceStrategies from "./admin/modules/namespaceStrategies";
|
import namespaceStrategies from "./admin/modules/namespaceStrategies";
|
||||||
import indexResources from "./modules/indexResource";
|
import indexResources from "./modules/indexResource";
|
||||||
|
import plugins from "./admin/plugins/modules/plugins";
|
||||||
|
|
||||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
import branches from "./repos/branches/modules/branches";
|
import branches from "./repos/branches/modules/branches";
|
||||||
@@ -42,7 +43,8 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
config,
|
config,
|
||||||
roles,
|
roles,
|
||||||
sources,
|
sources,
|
||||||
namespaceStrategies
|
namespaceStrategies,
|
||||||
|
plugins
|
||||||
});
|
});
|
||||||
|
|
||||||
return createStore(
|
return createStore(
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ export function getUiPluginsLink(state: Object) {
|
|||||||
return getLink(state, "uiPlugins");
|
return getLink(state, "uiPlugins");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPluginsLink(state: Object) {
|
||||||
|
return getLink(state, "plugins");
|
||||||
|
}
|
||||||
|
|
||||||
export function getMeLink(state: Object) {
|
export function getMeLink(state: Object) {
|
||||||
return getLink(state, "me");
|
return getLink(state, "me");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,12 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import injectSheet from "react-jss";
|
|
||||||
import type { Repository } from "@scm-manager/ui-types";
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
import { DateFromNow } from "@scm-manager/ui-components";
|
import { CardColumn, DateFromNow } from "@scm-manager/ui-components";
|
||||||
import RepositoryEntryLink from "./RepositoryEntryLink";
|
import RepositoryEntryLink from "./RepositoryEntryLink";
|
||||||
import classNames from "classnames";
|
|
||||||
import RepositoryAvatar from "./RepositoryAvatar";
|
import RepositoryAvatar from "./RepositoryAvatar";
|
||||||
|
|
||||||
const styles = {
|
|
||||||
inner: {
|
|
||||||
position: "relative",
|
|
||||||
pointerEvents: "none",
|
|
||||||
zIndex: 1
|
|
||||||
},
|
|
||||||
innerLink: {
|
|
||||||
pointerEvents: "all"
|
|
||||||
},
|
|
||||||
centerImage: {
|
|
||||||
marginTop: "0.8em",
|
|
||||||
marginLeft: "1em !important"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository,
|
repository: Repository
|
||||||
fullColumnWidth?: boolean,
|
|
||||||
// context props
|
|
||||||
classes: any
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class RepositoryEntry extends React.Component<Props> {
|
class RepositoryEntry extends React.Component<Props> {
|
||||||
@@ -83,53 +62,41 @@ class RepositoryEntry extends React.Component<Props> {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
createFooterLeft = (repository: Repository, repositoryLink: string) => {
|
||||||
const { repository, classes, fullColumnWidth } = this.props;
|
|
||||||
const repositoryLink = this.createLink(repository);
|
|
||||||
const halfColumn = fullColumnWidth ? "is-full" : "is-half";
|
|
||||||
const overlayLinkClass = fullColumnWidth
|
|
||||||
? "overlay-full-column"
|
|
||||||
: "overlay-half-column";
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={classNames(
|
|
||||||
"box",
|
|
||||||
"box-link-shadow",
|
|
||||||
"column",
|
|
||||||
"is-clipped",
|
|
||||||
halfColumn
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link className={classNames(overlayLinkClass)} to={repositoryLink} />
|
|
||||||
<article className={classNames("media", classes.inner)}>
|
|
||||||
<figure className={classNames(classes.centerImage, "media-left")}>
|
|
||||||
<RepositoryAvatar repository={repository} />
|
|
||||||
</figure>
|
|
||||||
<div className={classNames("media-content", "text-box")}>
|
|
||||||
<div className="content">
|
|
||||||
<p className="is-marginless">
|
|
||||||
<strong>{repository.name}</strong>
|
|
||||||
</p>
|
|
||||||
<p className={"shorten-text"}>{repository.description}</p>
|
|
||||||
</div>
|
|
||||||
<nav className="level is-mobile">
|
|
||||||
<div className="level-left">
|
|
||||||
{this.renderBranchesLink(repository, repositoryLink)}
|
{this.renderBranchesLink(repository, repositoryLink)}
|
||||||
{this.renderChangesetsLink(repository, repositoryLink)}
|
{this.renderChangesetsLink(repository, repositoryLink)}
|
||||||
{this.renderSourcesLink(repository, repositoryLink)}
|
{this.renderSourcesLink(repository, repositoryLink)}
|
||||||
{this.renderModifyLink(repository, repositoryLink)}
|
{this.renderModifyLink(repository, repositoryLink)}
|
||||||
</div>
|
</>
|
||||||
<div className="level-right is-hidden-mobile">
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
createFooterRight = (repository: Repository) => {
|
||||||
|
return (
|
||||||
<small className="level-item">
|
<small className="level-item">
|
||||||
<DateFromNow date={repository.creationDate} />
|
<DateFromNow date={repository.creationDate} />
|
||||||
</small>
|
</small>
|
||||||
</div>
|
);
|
||||||
</nav>
|
};
|
||||||
</div>
|
|
||||||
</article>
|
render() {
|
||||||
</div>
|
const { repository } = this.props;
|
||||||
|
const repositoryLink = this.createLink(repository);
|
||||||
|
const footerLeft = this.createFooterLeft(repository, repositoryLink);
|
||||||
|
const footerRight = this.createFooterRight(repository);
|
||||||
|
return (
|
||||||
|
<CardColumn
|
||||||
|
avatar={<RepositoryAvatar repository={repository} />}
|
||||||
|
title={repository.name}
|
||||||
|
description={repository.description}
|
||||||
|
link={repositoryLink}
|
||||||
|
footerLeft={footerLeft}
|
||||||
|
footerRight={footerRight}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectSheet(styles)(RepositoryEntry);
|
export default RepositoryEntry;
|
||||||
|
|||||||
@@ -1,92 +1,21 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { RepositoryGroup, Repository } from "@scm-manager/ui-types";
|
import { CardColumnGroup } from "@scm-manager/ui-components";
|
||||||
import injectSheet from "react-jss";
|
import type { RepositoryGroup } from "@scm-manager/ui-types";
|
||||||
import classNames from "classnames";
|
|
||||||
import RepositoryEntry from "./RepositoryEntry";
|
import RepositoryEntry from "./RepositoryEntry";
|
||||||
|
|
||||||
const styles = {
|
|
||||||
pointer: {
|
|
||||||
cursor: "pointer",
|
|
||||||
fontSize: "1.5rem"
|
|
||||||
},
|
|
||||||
repoGroup: {
|
|
||||||
marginBottom: "1em"
|
|
||||||
},
|
|
||||||
wrapper: {
|
|
||||||
padding: "0 0.75rem"
|
|
||||||
},
|
|
||||||
clearfix: {
|
|
||||||
clear: "both"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: RepositoryGroup,
|
group: RepositoryGroup
|
||||||
|
|
||||||
// context props
|
|
||||||
classes: any
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
class RepositoryGroupEntry extends React.Component<Props> {
|
||||||
collapsed: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
class RepositoryGroupEntry extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
collapsed: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleCollapse = () => {
|
|
||||||
this.setState(prevState => ({
|
|
||||||
collapsed: !prevState.collapsed
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
isLastEntry = (array: Repository[], index: number) => {
|
|
||||||
return index === array.length - 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
isLengthOdd = (array: Repository[]) => {
|
|
||||||
return array.length % 2 !== 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
isFullSize = (array: Repository[], index: number) => {
|
|
||||||
return this.isLastEntry(array, index) && this.isLengthOdd(array);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { group, classes } = this.props;
|
const { group } = this.props;
|
||||||
const { collapsed } = this.state;
|
const entries = group.repositories.map((repository, index) => {
|
||||||
|
return <RepositoryEntry repository={repository} key={index} />;
|
||||||
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
|
|
||||||
let content = null;
|
|
||||||
if (!collapsed) {
|
|
||||||
content = group.repositories.map((repository, index) => {
|
|
||||||
const fullColumnWidth = this.isFullSize(group.repositories, index);
|
|
||||||
return (
|
|
||||||
<RepositoryEntry repository={repository} fullColumnWidth={fullColumnWidth} key={index} />
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
return <CardColumnGroup name={group.name} elements={entries} />;
|
||||||
return (
|
|
||||||
<div className={classes.repoGroup}>
|
|
||||||
<h2>
|
|
||||||
<span className={classes.pointer} onClick={this.toggleCollapse}>
|
|
||||||
<i className={classNames("fa", icon)} /> {group.name}
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<hr />
|
|
||||||
<div className={classNames("columns", "is-multiline", classes.wrapper)}>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
<div className={classes.clearfix} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectSheet(styles)(RepositoryGroupEntry);
|
export default RepositoryGroupEntry;
|
||||||
|
|||||||
@@ -159,27 +159,31 @@ ul.is-separated {
|
|||||||
|
|
||||||
// multiline Columns
|
// multiline Columns
|
||||||
.columns.is-multiline {
|
.columns.is-multiline {
|
||||||
|
.column {
|
||||||
|
height: 120px;
|
||||||
|
|
||||||
|
.overlay-column {
|
||||||
|
position: absolute;
|
||||||
|
height: calc(120px - 1.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column.is-half {
|
.column.is-half {
|
||||||
width: calc(50% - 0.75rem);
|
width: calc(50% - 0.75rem);
|
||||||
max-height: 120px;
|
|
||||||
|
|
||||||
&:nth-child(odd) {
|
&:nth-child(odd) {
|
||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-half-column {
|
.overlay-column {
|
||||||
position: absolute;
|
|
||||||
height: calc(120px - 1.5rem);
|
|
||||||
width: calc(50% - 3rem);
|
width: calc(50% - 3rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.column.is-full {
|
|
||||||
.overlay-full-column {
|
.column.is-full .overlay-column {
|
||||||
position: absolute;
|
|
||||||
height: calc(120px - 0.5rem);
|
|
||||||
width: calc(100% - 1.5rem);
|
width: calc(100% - 1.5rem);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.column.is-half {
|
.column.is-half {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -188,9 +192,25 @@ ul.is-separated {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-half-column {
|
.overlay-column {
|
||||||
position: absolute;
|
width: calc(100% - 1.5rem);
|
||||||
height: calc(120px - 0.5rem);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content.is-plugin-page {
|
||||||
|
.columns.is-multiline {
|
||||||
|
.column.is-half .overlay-column {
|
||||||
|
width: calc(37.5% - 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column.is-full .overlay-column {
|
||||||
|
width: calc(75% - 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.column.is-half .overlay-column,
|
||||||
|
.column.is-full .overlay-column {
|
||||||
width: calc(100% - 1.5rem);
|
width: calc(100% - 1.5rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.apache.shiro.SecurityUtils;
|
|||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.config.ConfigurationPermissions;
|
import sonia.scm.config.ConfigurationPermissions;
|
||||||
import sonia.scm.group.GroupPermissions;
|
import sonia.scm.group.GroupPermissions;
|
||||||
|
import sonia.scm.plugin.PluginPermissions;
|
||||||
import sonia.scm.repository.RepositoryRolePermissions;
|
import sonia.scm.repository.RepositoryRolePermissions;
|
||||||
import sonia.scm.security.PermissionPermissions;
|
import sonia.scm.security.PermissionPermissions;
|
||||||
import sonia.scm.user.UserPermissions;
|
import sonia.scm.user.UserPermissions;
|
||||||
@@ -34,11 +35,15 @@ public class IndexDtoGenerator extends HalAppenderMapper {
|
|||||||
List<Link> autoCompleteLinks = Lists.newArrayList();
|
List<Link> autoCompleteLinks = Lists.newArrayList();
|
||||||
builder.self(resourceLinks.index().self());
|
builder.self(resourceLinks.index().self());
|
||||||
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
|
builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
|
||||||
|
|
||||||
if (SecurityUtils.getSubject().isAuthenticated()) {
|
if (SecurityUtils.getSubject().isAuthenticated()) {
|
||||||
builder.single(
|
builder.single(
|
||||||
link("me", resourceLinks.me().self()),
|
link("me", resourceLinks.me().self()),
|
||||||
link("logout", resourceLinks.authentication().logout())
|
link("logout", resourceLinks.authentication().logout())
|
||||||
);
|
);
|
||||||
|
if (PluginPermissions.read().isPermitted()) {
|
||||||
|
builder.single(link("plugins", resourceLinks.pluginCollection().self()));
|
||||||
|
}
|
||||||
if (UserPermissions.list().isPermitted()) {
|
if (UserPermissions.list().isPermitted()) {
|
||||||
builder.single(link("users", resourceLinks.userCollection().self()));
|
builder.single(link("users", resourceLinks.userCollection().self()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class PluginDto extends HalRepresentation {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String type;
|
||||||
|
private String version;
|
||||||
|
private String author;
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
public PluginDto(Links links) {
|
||||||
|
add(links);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import de.otto.edison.hal.Embedded;
|
||||||
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import sonia.scm.plugin.PluginWrapper;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||||
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
|
public class PluginDtoCollectionMapper {
|
||||||
|
|
||||||
|
private final ResourceLinks resourceLinks;
|
||||||
|
private final PluginDtoMapper mapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper) {
|
||||||
|
this.resourceLinks = resourceLinks;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HalRepresentation map(Collection<PluginWrapper> plugins) {
|
||||||
|
List<PluginDto> dtos = plugins.stream().map(mapper::map).collect(toList());
|
||||||
|
return new HalRepresentation(createLinks(), embedDtos(dtos));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Links createLinks() {
|
||||||
|
String baseUrl = resourceLinks.pluginCollection().self();
|
||||||
|
|
||||||
|
Links.Builder linksBuilder = linkingTo()
|
||||||
|
.with(Links.linkingTo().self(baseUrl).build());
|
||||||
|
return linksBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Embedded embedDtos(List<PluginDto> dtos) {
|
||||||
|
return embeddedBuilder()
|
||||||
|
.with("plugins", dtos)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import sonia.scm.plugin.PluginWrapper;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
|
|
||||||
|
public class PluginDtoMapper {
|
||||||
|
|
||||||
|
private final ResourceLinks resourceLinks;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PluginDtoMapper(ResourceLinks resourceLinks) {
|
||||||
|
this.resourceLinks = resourceLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginDto map(PluginWrapper plugin) {
|
||||||
|
Links.Builder linksBuilder = linkingTo()
|
||||||
|
.self(resourceLinks.plugin()
|
||||||
|
.self(plugin.getPlugin().getInformation().getId(false)));
|
||||||
|
|
||||||
|
PluginDto pluginDto = new PluginDto(linksBuilder.build());
|
||||||
|
pluginDto.setName(plugin.getPlugin().getInformation().getName());
|
||||||
|
pluginDto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Miscellaneous");
|
||||||
|
pluginDto.setVersion(plugin.getPlugin().getInformation().getVersion());
|
||||||
|
pluginDto.setAuthor(plugin.getPlugin().getInformation().getAuthor());
|
||||||
|
pluginDto.setDescription(plugin.getPlugin().getInformation().getDescription());
|
||||||
|
|
||||||
|
return pluginDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||||
|
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||||
|
import sonia.scm.plugin.Plugin;
|
||||||
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
import sonia.scm.plugin.PluginPermissions;
|
||||||
|
import sonia.scm.plugin.PluginWrapper;
|
||||||
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
|
public class PluginResource {
|
||||||
|
|
||||||
|
private final PluginLoader pluginLoader;
|
||||||
|
private final PluginDtoCollectionMapper collectionMapper;
|
||||||
|
private final PluginDtoMapper mapper;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PluginResource(PluginLoader pluginLoader, PluginDtoCollectionMapper collectionMapper, PluginDtoMapper mapper) {
|
||||||
|
this.pluginLoader = pluginLoader;
|
||||||
|
this.collectionMapper = collectionMapper;
|
||||||
|
this.mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a collection of installed plugins.
|
||||||
|
*
|
||||||
|
* @return collection of installed plugins.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("")
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
@TypeHint(CollectionDto.class)
|
||||||
|
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
||||||
|
public Response getInstalledPlugins() {
|
||||||
|
PluginPermissions.read().check();
|
||||||
|
List<PluginWrapper> plugins = new ArrayList<>(pluginLoader.getInstalledPlugins());
|
||||||
|
return Response.ok(collectionMapper.map(plugins)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the installed plugin with the given id.
|
||||||
|
*
|
||||||
|
* @param id id of plugin
|
||||||
|
*
|
||||||
|
* @return installed plugin with specified id
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
@StatusCodes({
|
||||||
|
@ResponseCode(code = 200, condition = "success"),
|
||||||
|
@ResponseCode(code = 404, condition = "not found"),
|
||||||
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
|
})
|
||||||
|
@TypeHint(PluginDto.class)
|
||||||
|
@Produces(VndMediaType.PLUGIN)
|
||||||
|
public Response getInstalledPlugin(@PathParam("id") String id) {
|
||||||
|
PluginPermissions.read().check();
|
||||||
|
Optional<PluginDto> pluginDto = pluginLoader.getInstalledPlugins()
|
||||||
|
.stream()
|
||||||
|
.filter(plugin -> id.equals(plugin.getPlugin().getInformation().getId(false)))
|
||||||
|
.map(mapper::map)
|
||||||
|
.findFirst();
|
||||||
|
if (pluginDto.isPresent()) {
|
||||||
|
return Response.ok(pluginDto.get()).build();
|
||||||
|
} else {
|
||||||
|
throw notFound(entity(Plugin.class, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
|
||||||
|
@Path("v2/")
|
||||||
|
public class PluginRootResource {
|
||||||
|
|
||||||
|
private Provider<PluginResource> pluginResourceProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PluginRootResource(Provider<PluginResource> pluginResourceProvider) {
|
||||||
|
this.pluginResourceProvider = pluginResourceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("plugins")
|
||||||
|
public PluginResource plugins() {
|
||||||
|
return pluginResourceProvider.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -651,6 +651,38 @@ class ResourceLinks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PluginLinks plugin() {
|
||||||
|
return new PluginLinks(scmPathInfoStore.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PluginLinks {
|
||||||
|
private final LinkBuilder pluginLinkBuilder;
|
||||||
|
|
||||||
|
PluginLinks(ScmPathInfo pathInfo) {
|
||||||
|
pluginLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PluginResource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
String self(String id) {
|
||||||
|
return pluginLinkBuilder.method("plugins").parameters().method("getInstalledPlugin").parameters(id).href();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginCollectionLinks pluginCollection() {
|
||||||
|
return new PluginCollectionLinks(scmPathInfoStore.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
static class PluginCollectionLinks {
|
||||||
|
private final LinkBuilder pluginCollectionLinkBuilder;
|
||||||
|
|
||||||
|
PluginCollectionLinks(ScmPathInfo pathInfo) {
|
||||||
|
pluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PluginResource.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
String self() {
|
||||||
|
return pluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public AuthenticationLinks authentication() {
|
public AuthenticationLinks authentication() {
|
||||||
return new AuthenticationLinks(scmPathInfoStore.get());
|
return new AuthenticationLinks(scmPathInfoStore.get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,13 @@
|
|||||||
<value>configuration:read,write:*</value>
|
<value>configuration:read,write:*</value>
|
||||||
</permission>
|
</permission>
|
||||||
<permission>
|
<permission>
|
||||||
<value>repositoryRole:write</value>
|
<value>repositoryRole:read,write</value>
|
||||||
|
</permission>
|
||||||
|
<permission>
|
||||||
|
<value>plugin:read</value>
|
||||||
|
</permission>
|
||||||
|
<permission>
|
||||||
|
<value>plugin:read,write</value>
|
||||||
</permission>
|
</permission>
|
||||||
|
|
||||||
</permissions>
|
</permissions>
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,16 @@
|
|||||||
"description": "Kann benutzerdefinierte Rollen und deren Berechtigungen erstellen, ändern und löschen"
|
"description": "Kann benutzerdefinierte Rollen und deren Berechtigungen erstellen, ändern und löschen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugin": {
|
||||||
|
"read": {
|
||||||
|
"displayName": "Alle Plugins lesen",
|
||||||
|
"description": "Darf alle installierten und verfügbaren Plugins lesen"
|
||||||
|
},
|
||||||
|
"read,write": {
|
||||||
|
"displayName": "Alle Plugins lesen und verwalten",
|
||||||
|
"description": "Darf alle installierten und verfügbaren Plugins lesen und verwalten"
|
||||||
|
}
|
||||||
|
},
|
||||||
"unknown": "Unbekannte Berechtigung"
|
"unknown": "Unbekannte Berechtigung"
|
||||||
},
|
},
|
||||||
"verbs": {
|
"verbs": {
|
||||||
|
|||||||
@@ -66,6 +66,16 @@
|
|||||||
"description": "May create, modify and delete custom repository roles and their permissions"
|
"description": "May create, modify and delete custom repository roles and their permissions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugin": {
|
||||||
|
"read": {
|
||||||
|
"displayName": "Read all plugins",
|
||||||
|
"description": "May see all installed and available plugins"
|
||||||
|
},
|
||||||
|
"read,write": {
|
||||||
|
"displayName": "Read and manage all plugins",
|
||||||
|
"description": "May see and manage all installed and available plugins"
|
||||||
|
}
|
||||||
|
},
|
||||||
"unknown": "Unknown permission"
|
"unknown": "Unknown permission"
|
||||||
},
|
},
|
||||||
"verbs": {
|
"verbs": {
|
||||||
|
|||||||
Reference in New Issue
Block a user