From 4b3a897a32b537f1b2f24826f2cfbe6542aad50c Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Wed, 3 Jul 2019 09:02:44 +0200 Subject: [PATCH 01/30] added PluginsOverview Page --- scm-ui/public/locales/de/admin.json | 10 ++++ scm-ui/public/locales/en/admin.json | 10 ++++ scm-ui/src/admin/containers/Admin.js | 44 ++++++++++++++ scm-ui/src/admin/containers/AdminDetails.js | 17 +++--- .../plugins/containers/PluginsOverview.js | 57 +++++++++++++++++++ .../src/admin/roles/containers/PluginsList.js | 0 6 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 scm-ui/src/admin/plugins/containers/PluginsOverview.js create mode 100644 scm-ui/src/admin/roles/containers/PluginsList.js diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index d7ced4ce70..1cae12b544 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -10,6 +10,16 @@ "currentAppVersion": "Aktuelle Software-Versionsnummer" } }, + "plugins": { + "title": "Plugins", + "installedSubtitle": "Installierte Plugins", + "availableSubtitle": "Verfügbare Plugins", + "menu": { + "pluginsNavLink": "Plugins", + "installedNavLink": "Installiert", + "availableNavLink": "Verfügbar" + } + }, "repositoryRole": { "navLink": "Berechtigungsrollen", "title": "Berechtigungsrollen", diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 83b13990c0..2df7b80386 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -10,6 +10,16 @@ "currentAppVersion": "Current Application Version" } }, + "plugins": { + "title": "Plugins", + "installedSubtitle": "Installed Plugins", + "availableSubtitle": "Available Plugins", + "menu": { + "pluginsNavLink": "Plugins", + "installedNavLink": "Installed", + "availableNavLink": "Available" + } + }, "repositoryRole": { "navLink": "Permission Roles", "title": "Permission Roles", diff --git a/scm-ui/src/admin/containers/Admin.js b/scm-ui/src/admin/containers/Admin.js index 40ab00f95a..b00cc9163c 100644 --- a/scm-ui/src/admin/containers/Admin.js +++ b/scm-ui/src/admin/containers/Admin.js @@ -10,6 +10,7 @@ import type { Links } from "@scm-manager/ui-types"; import { Page, Navigation, NavLink, Section, SubNavigation } from "@scm-manager/ui-components"; import { getLinks } from "../../modules/indexResource"; import AdminDetails from "./AdminDetails"; +import PluginsOverview from "../plugins/containers/PluginsOverview"; import GlobalConfig from "./GlobalConfig"; import RepositoryRoles from "../roles/containers/RepositoryRoles"; import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole"; @@ -62,6 +63,35 @@ class Admin extends React.Component { + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> ( @@ -106,6 +136,20 @@ class Admin extends React.Component { icon="fas fa-info-circle" label={t("admin.menu.informationNavLink")} /> + + + + { return ; } - return <> - - <Subtitle subtitle={this.props.version}/> - </>; + return ( + <> + <Title title={t("admin.information.currentAppVersion")} /> + <Subtitle subtitle={this.props.version} /> + </> + ); } } diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js new file mode 100644 index 0000000000..e0b82817c8 --- /dev/null +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -0,0 +1,57 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { Loading, Title, Subtitle, LinkPaginator, Notification } from "@scm-manager/ui-components"; +import PluginsList from "../components/PluginsList"; + +type Props = { + loading: boolean, + error: Error, + baseUrl: string, + installed: boolean, + + // context objects + t: string => string +}; + +class PluginsOverview extends React.Component<Props> { + render() { + const { loading, installed, t } = this.props; + + if (loading) { + return <Loading />; + } + + return ( + <> + <Title title={t("plugins.title")} /> + <Subtitle + subtitle={ + installed + ? t("plugins.installedSubtitle") + : t("plugins.availableSubtitle") + } + /> + {this.renderPluginsList()} + </> + ); + } + + renderPluginsList() { + const { collection, page, t } = this.props; + + if (collection._embedded && collection._embedded.plugins.length > 0) { + return ( + <> + <PluginsList plugins={collection._embedded.plugins} /> + <LinkPaginator collection={collection} page={page} /> + </> + ); + } + return ( + <Notification type="info">{t("plugins.noPlugins")}</Notification> + ); + } +} + +export default translate("admin")(PluginsOverview); diff --git a/scm-ui/src/admin/roles/containers/PluginsList.js b/scm-ui/src/admin/roles/containers/PluginsList.js new file mode 100644 index 0000000000..e69de29bb2 From 088afc6b02f3eff679fbe759035171b6c9eb3f02 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 3 Jul 2019 09:35:15 +0200 Subject: [PATCH 02/30] added PluginList components --- .../packages/ui-types/src/Plugin.js | 23 +++ .../packages/ui-types/src/index.js | 2 + .../admin/plugins/components/PluginAvatar.js | 22 +++ .../admin/plugins/components/PluginEntry.js | 135 ++++++++++++++++++ .../admin/plugins/components/PluginsList.js | 24 ++++ 5 files changed, 206 insertions(+) create mode 100644 scm-ui-components/packages/ui-types/src/Plugin.js create mode 100644 scm-ui/src/admin/plugins/components/PluginAvatar.js create mode 100644 scm-ui/src/admin/plugins/components/PluginEntry.js create mode 100644 scm-ui/src/admin/plugins/components/PluginsList.js diff --git a/scm-ui-components/packages/ui-types/src/Plugin.js b/scm-ui-components/packages/ui-types/src/Plugin.js new file mode 100644 index 0000000000..e36576ccbc --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/Plugin.js @@ -0,0 +1,23 @@ +//@flow +import type { PagedCollection, Links } from "./hal"; + +export type Plugin = { + namespace: string, + name: string, + type: string, + description?: string, + creationDate?: string, + lastModified?: string, + _links: Links +}; + +export type PluginCollection = PagedCollection & { + _embedded: { + plugins: Plugin[] | string[] + } +}; + +export type PluginGroup = { + name: string, + plugins: Plugin[] +}; diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index 4024710300..ba2b9f5481 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -25,6 +25,8 @@ export type { SubRepository, File } from "./Sources"; export type { SelectValue, AutocompleteObject } from "./Autocomplete"; +export type { Plugin, PluginCollection, PluginGroup } from "./Plugin"; + export type { RepositoryRole } from "./RepositoryRole"; export type { NamespaceStrategies } from "./NamespaceStrategies"; diff --git a/scm-ui/src/admin/plugins/components/PluginAvatar.js b/scm-ui/src/admin/plugins/components/PluginAvatar.js new file mode 100644 index 0000000000..10408f14bd --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginAvatar.js @@ -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> + ); + } +} diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js new file mode 100644 index 0000000000..77ae84b937 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -0,0 +1,135 @@ +//@flow +import React from "react"; +import { Link } from "react-router-dom"; +import injectSheet from "react-jss"; +import type { Repository } from "@scm-manager/ui-types"; +import { DateFromNow } from "@scm-manager/ui-components"; +import RepositoryEntryLink from "./RepositoryEntryLink"; +import classNames from "classnames"; +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 = { + repository: Repository, + fullColumnWidth?: boolean, + // context props + classes: any +}; + +class PluginEntry extends React.Component<Props> { + createLink = (repository: Repository) => { + return `/repo/${repository.namespace}/${repository.name}`; + }; + + renderBranchesLink = (repository: Repository, repositoryLink: string) => { + if (repository._links["branches"]) { + return ( + <RepositoryEntryLink + iconClass="fas fa-code-branch fa-lg" + to={repositoryLink + "/branches"} + /> + ); + } + return null; + }; + + renderChangesetsLink = (repository: Repository, repositoryLink: string) => { + if (repository._links["changesets"]) { + return ( + <RepositoryEntryLink + iconClass="fas fa-exchange-alt fa-lg" + to={repositoryLink + "/changesets"} + /> + ); + } + return null; + }; + + renderSourcesLink = (repository: Repository, repositoryLink: string) => { + if (repository._links["sources"]) { + return ( + <RepositoryEntryLink + iconClass="fa-code fa-lg" + to={repositoryLink + "/sources"} + /> + ); + } + return null; + }; + + renderModifyLink = (repository: Repository, repositoryLink: string) => { + if (repository._links["update"]) { + return ( + <RepositoryEntryLink + iconClass="fa-cog fa-lg" + to={repositoryLink + "/settings/general"} + /> + ); + } + return null; + }; + + render() { + 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 ( + <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.renderChangesetsLink(repository, repositoryLink)} + {this.renderSourcesLink(repository, repositoryLink)} + {this.renderModifyLink(repository, repositoryLink)} + </div> + <div className="level-right is-hidden-mobile"> + <small className="level-item"> + <DateFromNow date={repository.creationDate} /> + </small> + </div> + </nav> + </div> + </article> + </div> + ); + } +} + +export default injectSheet(styles)(PluginEntry); diff --git a/scm-ui/src/admin/plugins/components/PluginsList.js b/scm-ui/src/admin/plugins/components/PluginsList.js new file mode 100644 index 0000000000..d4c99be888 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginsList.js @@ -0,0 +1,24 @@ +//@flow +import React from "react"; +import type { Plugins } from "@scm-manager/ui-types"; + +type Props = { + plugins: Plugins[] +}; + +class RepositoryList extends React.Component<Props> { + render() { + const { plugins } = this.props; + + const groups = groupByNamespace(plugins); + return ( + <div className="content"> + {groups.map(group => { + return <PluginEntry group={group} key={group.name} />; + })} + </div> + ); + } +} + +export default RepositoryList; From a03a666afe61d0a61369dca09b486ce8971948e4 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 3 Jul 2019 16:38:07 +0200 Subject: [PATCH 03/30] added more information too PluginDto --- .../java/sonia/scm/api/v2/resources/UIPluginDto.java | 11 +++++------ .../sonia/scm/api/v2/resources/UIPluginDtoMapper.java | 11 +++++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java index dfed9a3612..ec77afa0ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java @@ -6,16 +6,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -@Getter @Setter @NoArgsConstructor +@Getter @Setter public class UIPluginDto extends HalRepresentation { private String name; private Iterable<String> bundles; - - public UIPluginDto(String name, Iterable<String> bundles) { - this.name = name; - this.bundles = bundles; - } + private String type; + private String version; + private String author; + private String description; @Override protected HalRepresentation add(Links links) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 10ae79b5bf..572feafcdf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -26,10 +26,13 @@ public class UIPluginDtoMapper { } public UIPluginDto map(PluginWrapper plugin) { - UIPluginDto dto = new UIPluginDto( - plugin.getPlugin().getInformation().getName(), - getScriptResources(plugin) - ); + UIPluginDto dto = new UIPluginDto(); + dto.setName(plugin.getPlugin().getInformation().getName()); + dto.setBundles(getScriptResources(plugin)); + dto.setType("42"); + dto.setVersion(plugin.getPlugin().getInformation().getVersion()); + dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); + dto.setDescription(plugin.getPlugin().getInformation().getDescription()); Links.Builder linksBuilder = linkingTo() .self(resourceLinks.uiPlugin() From df501b162dda7bd0c9f64255dc6d0e696b1a1b3a Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Wed, 3 Jul 2019 16:42:36 +0200 Subject: [PATCH 04/30] pluginsOverview goes api data --- .../packages/ui-types/src/Plugin.js | 5 +-- scm-ui/public/locales/de/admin.json | 3 +- scm-ui/public/locales/en/admin.json | 3 +- .../plugins/containers/PluginsOverview.js | 45 ++++++++++++++++++- scm-webapp/pom.xml | 32 ++++++------- 5 files changed, 65 insertions(+), 23 deletions(-) diff --git a/scm-ui-components/packages/ui-types/src/Plugin.js b/scm-ui-components/packages/ui-types/src/Plugin.js index e36576ccbc..566d52e76a 100644 --- a/scm-ui-components/packages/ui-types/src/Plugin.js +++ b/scm-ui-components/packages/ui-types/src/Plugin.js @@ -1,8 +1,7 @@ //@flow -import type { PagedCollection, Links } from "./hal"; +import type { Collection, Links } from "./hal"; export type Plugin = { - namespace: string, name: string, type: string, description?: string, @@ -11,7 +10,7 @@ export type Plugin = { _links: Links }; -export type PluginCollection = PagedCollection & { +export type PluginCollection = Collection & { _embedded: { plugins: Plugin[] | string[] } diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 1cae12b544..00793b6dce 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -18,7 +18,8 @@ "pluginsNavLink": "Plugins", "installedNavLink": "Installiert", "availableNavLink": "Verfügbar" - } + }, + "noPlugins": "Keine Plugins gefunden." }, "repositoryRole": { "navLink": "Berechtigungsrollen", diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 2df7b80386..a5697ea4fe 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -18,7 +18,8 @@ "pluginsNavLink": "Plugins", "installedNavLink": "Installed", "availableNavLink": "Available" - } + }, + "noPlugins": "No plugins found." }, "repositoryRole": { "navLink": "Permission Roles", diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js index e0b82817c8..391d205a99 100644 --- a/scm-ui/src/admin/plugins/containers/PluginsOverview.js +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -1,20 +1,40 @@ // @flow import React from "react"; +import {connect} from "react-redux"; import { translate } from "react-i18next"; +import type { PluginCollection } from "@scm-manager/ui-types"; import { Loading, Title, Subtitle, LinkPaginator, Notification } from "@scm-manager/ui-components"; +import { + fetchPluginsByLink, + getFetchPluginsFailure, + getPluginCollection, + isFetchPluginsPending +} from "../modules/plugins"; import PluginsList from "../components/PluginsList"; +import { getUiPluginsLink } from "../../../modules/indexResource"; type Props = { loading: boolean, error: Error, + collection: PluginCollection, + page: number, baseUrl: string, installed: boolean, + pluginsLink: string, // context objects - t: string => string + 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, installed, t } = this.props; @@ -54,4 +74,25 @@ class PluginsOverview extends React.Component<Props> { } } -export default translate("admin")(PluginsOverview); +const mapStateToProps = (state) => { + const collection = getPluginCollection(state); + const loading = isFetchPluginsPending(state); + const error = getFetchPluginsFailure(state); + const pluginsLink = getUiPluginsLink(state); + return { + collection, + loading, + error, + pluginsLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchPluginsByLink: (link: string) => { + dispatch(fetchPluginsByLink(link)); + } + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(translate("admin")(PluginsOverview)); diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index cc2d9b99da..a74d8dc429 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -34,16 +34,16 @@ </dependency> <!-- fix javadoc --> - - <dependency> + + <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> <scope>provided</scope> </dependency> - + <!-- scm --> - + <dependency> <groupId>sonia.scm</groupId> <artifactId>scm-core</artifactId> @@ -63,7 +63,7 @@ <artifactId>shiro-web</artifactId> <version>${shiro.version}</version> </dependency> - + <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-guice</artifactId> @@ -204,13 +204,13 @@ <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> - + <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> - + <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> @@ -237,13 +237,13 @@ <artifactId>commons-codec</artifactId> <version>1.9</version> </dependency> - + <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency> - + <dependency> <groupId>com.cronutils</groupId> <artifactId>cron-utils</artifactId> @@ -312,7 +312,7 @@ <version>${selenium.version}</version> <scope>test</scope> </dependency> - + <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>htmlunit-driver</artifactId> @@ -386,16 +386,16 @@ <version>2.0.0-SNAPSHOT</version> <scope>test</scope> </dependency> - + <!-- global excludes --> - + <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> <scope>provided</scope> </dependency> - + <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> @@ -419,7 +419,7 @@ <build> <plugins> - + <plugin> <groupId>com.mycila.maven-license-plugin</groupId> <artifactId>maven-license-plugin</artifactId> @@ -459,7 +459,7 @@ </execution> </executions> </plugin> - + <plugin> <groupId>sonia.scm.maven</groupId> <artifactId>smp-maven-plugin</artifactId> @@ -947,6 +947,6 @@ </profile> </profiles> - + </project> From f1dd467b896bfc0a196f402d9f67a2e5a2e6fb80 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 4 Jul 2019 16:33:01 +0200 Subject: [PATCH 05/30] removed brace in string typed className-Prop --- .../packages/ui-components/src/LinkPaginator.js | 4 ++-- .../packages/ui-components/src/StatePaginator.js | 8 ++++---- scm-ui/src/repos/components/list/RepositoryEntry.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/LinkPaginator.js b/scm-ui-components/packages/ui-components/src/LinkPaginator.js index a662272b11..a5d67816d0 100644 --- a/scm-ui-components/packages/ui-components/src/LinkPaginator.js +++ b/scm-ui-components/packages/ui-components/src/LinkPaginator.js @@ -25,7 +25,7 @@ class LinkPaginator extends React.Component<Props> { renderFirstButton() { return ( <Button - className={"pagination-link"} + className="pagination-link" label={"1"} disabled={false} link={this.addFilterToLink("1")} @@ -69,7 +69,7 @@ class LinkPaginator extends React.Component<Props> { const { collection } = this.props; return ( <Button - className={"pagination-link"} + className="pagination-link" label={`${collection.pageTotal}`} disabled={false} link={this.addFilterToLink(`${collection.pageTotal}`)} diff --git a/scm-ui-components/packages/ui-components/src/StatePaginator.js b/scm-ui-components/packages/ui-components/src/StatePaginator.js index 04f70ead52..c3794d10f3 100644 --- a/scm-ui-components/packages/ui-components/src/StatePaginator.js +++ b/scm-ui-components/packages/ui-components/src/StatePaginator.js @@ -17,7 +17,7 @@ class StatePaginator extends React.Component<Props> { renderFirstButton() { return ( <Button - className={"pagination-link"} + className="pagination-link" label={"1"} disabled={false} action={() => this.updateCurrentPage(1)} @@ -35,7 +35,7 @@ class StatePaginator extends React.Component<Props> { return ( <Button - className={"pagination-previous"} + className="pagination-previous" label={label ? label : previousPage.toString()} disabled={!this.hasLink("prev")} action={() => this.updateCurrentPage(previousPage)} @@ -53,7 +53,7 @@ class StatePaginator extends React.Component<Props> { const nextPage = page + 1; return ( <Button - className={"pagination-next"} + className="pagination-next" label={label ? label : nextPage.toString()} disabled={!this.hasLink("next")} action={() => this.updateCurrentPage(nextPage)} @@ -65,7 +65,7 @@ class StatePaginator extends React.Component<Props> { const { collection } = this.props; return ( <Button - className={"pagination-link"} + className="pagination-link" label={`${collection.pageTotal}`} disabled={false} action={() => this.updateCurrentPage(collection.pageTotal)} diff --git a/scm-ui/src/repos/components/list/RepositoryEntry.js b/scm-ui/src/repos/components/list/RepositoryEntry.js index 0a7cf0d434..eb3b3d95f6 100644 --- a/scm-ui/src/repos/components/list/RepositoryEntry.js +++ b/scm-ui/src/repos/components/list/RepositoryEntry.js @@ -110,7 +110,7 @@ class RepositoryEntry extends React.Component<Props> { <p className="is-marginless"> <strong>{repository.name}</strong> </p> - <p className={"shorten-text"}>{repository.description}</p> + <p className="shorten-text">{repository.description}</p> </div> <nav className="level is-mobile"> <div className="level-left"> From 3844e008ba909938a4cf1a1aeb0370d2f2aaa349 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 4 Jul 2019 16:33:44 +0200 Subject: [PATCH 06/30] corrected Plugin ui-type --- scm-ui-components/packages/ui-types/src/Plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-types/src/Plugin.js b/scm-ui-components/packages/ui-types/src/Plugin.js index 566d52e76a..bb9c5e7d88 100644 --- a/scm-ui-components/packages/ui-types/src/Plugin.js +++ b/scm-ui-components/packages/ui-types/src/Plugin.js @@ -4,9 +4,9 @@ import type { Collection, Links } from "./hal"; export type Plugin = { name: string, type: string, + version: string, + author: string, description?: string, - creationDate?: string, - lastModified?: string, _links: Links }; From 0f3c47ceb492b208343d7e15a296b77ffd3808f3 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 4 Jul 2019 17:31:15 +0200 Subject: [PATCH 07/30] implemented pluginsList --- .../admin/plugins/components/PluginEntry.js | 111 +++++------------- .../plugins/components/PluginGroupEntry.js | 96 +++++++++++++++ .../admin/plugins/components/PluginsList.js | 14 ++- .../plugins/components/groupByCategory.js | 39 ++++++ .../src/admin/roles/containers/PluginsList.js | 0 scm-ui/styles/scm.scss | 14 ++- 6 files changed, 185 insertions(+), 89 deletions(-) create mode 100644 scm-ui/src/admin/plugins/components/PluginGroupEntry.js create mode 100644 scm-ui/src/admin/plugins/components/groupByCategory.js delete mode 100644 scm-ui/src/admin/roles/containers/PluginsList.js diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js index 77ae84b937..b12c8dd1e1 100644 --- a/scm-ui/src/admin/plugins/components/PluginEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -2,11 +2,9 @@ import React from "react"; import { Link } from "react-router-dom"; import injectSheet from "react-jss"; -import type { Repository } from "@scm-manager/ui-types"; -import { DateFromNow } from "@scm-manager/ui-components"; -import RepositoryEntryLink from "./RepositoryEntryLink"; import classNames from "classnames"; -import RepositoryAvatar from "./RepositoryAvatar"; +import type { Plugin } from "@scm-manager/ui-types"; +import PluginAvatar from "./PluginAvatar"; const styles = { inner: { @@ -14,82 +12,31 @@ const styles = { pointerEvents: "none", zIndex: 1 }, - innerLink: { - pointerEvents: "all" - }, centerImage: { marginTop: "0.8em", marginLeft: "1em !important" + }, + marginBottom: { + marginBottom: "0.75rem !important" } }; type Props = { - repository: Repository, + plugin: Plugin, fullColumnWidth?: boolean, + // context props classes: any }; class PluginEntry extends React.Component<Props> { - createLink = (repository: Repository) => { - return `/repo/${repository.namespace}/${repository.name}`; - }; - - renderBranchesLink = (repository: Repository, repositoryLink: string) => { - if (repository._links["branches"]) { - return ( - <RepositoryEntryLink - iconClass="fas fa-code-branch fa-lg" - to={repositoryLink + "/branches"} - /> - ); - } - return null; - }; - - renderChangesetsLink = (repository: Repository, repositoryLink: string) => { - if (repository._links["changesets"]) { - return ( - <RepositoryEntryLink - iconClass="fas fa-exchange-alt fa-lg" - to={repositoryLink + "/changesets"} - /> - ); - } - return null; - }; - - renderSourcesLink = (repository: Repository, repositoryLink: string) => { - if (repository._links["sources"]) { - return ( - <RepositoryEntryLink - iconClass="fa-code fa-lg" - to={repositoryLink + "/sources"} - /> - ); - } - return null; - }; - - renderModifyLink = (repository: Repository, repositoryLink: string) => { - if (repository._links["update"]) { - return ( - <RepositoryEntryLink - iconClass="fa-cog fa-lg" - to={repositoryLink + "/settings/general"} - /> - ); - } - return null; - }; - render() { - const { repository, classes, fullColumnWidth } = this.props; - const repositoryLink = this.createLink(repository); + const { plugin, classes, fullColumnWidth } = this.props; const halfColumn = fullColumnWidth ? "is-full" : "is-half"; const overlayLinkClass = fullColumnWidth ? "overlay-full-column" : "overlay-half-column"; + // TODO: Add link to plugin page below return ( <div className={classNames( @@ -100,31 +47,35 @@ class PluginEntry extends React.Component<Props> { halfColumn )} > - <Link className={classNames(overlayLinkClass)} to={repositoryLink} /> + <Link + className={classNames(overlayLinkClass, "is-plugin-page")} + to="#" + /> <article className={classNames("media", classes.inner)}> <figure className={classNames(classes.centerImage, "media-left")}> - <RepositoryAvatar repository={repository} /> + <PluginAvatar plugin={plugin} /> </figure> <div className={classNames("media-content", "text-box")}> <div className="content"> - <p className="is-marginless"> - <strong>{repository.name}</strong> + <nav + className={classNames( + "level", + "is-mobile", + classes.marginBottom + )} + > + <div className="level-left"> + <strong>{plugin.name}</strong> + </div> + <div className="level-right is-hidden-mobile"> + {plugin.version} + </div> + </nav> + <p className="shorten-text is-marginless">{plugin.description}</p> + <p> + <small>{plugin.author}</small> </p> - <p className={"shorten-text"}>{repository.description}</p> </div> - <nav className="level is-mobile"> - <div className="level-left"> - {this.renderBranchesLink(repository, repositoryLink)} - {this.renderChangesetsLink(repository, repositoryLink)} - {this.renderSourcesLink(repository, repositoryLink)} - {this.renderModifyLink(repository, repositoryLink)} - </div> - <div className="level-right is-hidden-mobile"> - <small className="level-item"> - <DateFromNow date={repository.creationDate} /> - </small> - </div> - </nav> </div> </article> </div> diff --git a/scm-ui/src/admin/plugins/components/PluginGroupEntry.js b/scm-ui/src/admin/plugins/components/PluginGroupEntry.js new file mode 100644 index 0000000000..87076f6eaf --- /dev/null +++ b/scm-ui/src/admin/plugins/components/PluginGroupEntry.js @@ -0,0 +1,96 @@ +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import type { PluginGroup, Plugin } from "@scm-manager/ui-types"; +import PluginEntry from "./PluginEntry"; + +const styles = { + pointer: { + cursor: "pointer", + fontSize: "1.5rem" + }, + pluginGroup: { + marginBottom: "1em" + }, + wrapper: { + padding: "0 0.75rem" + }, + clearfix: { + clear: "both" + } +}; + +type Props = { + group: PluginGroup, + + // context props + classes: any +}; + +type State = { + collapsed: boolean +}; + +class PluginGroupEntry extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + this.state = { + collapsed: false + }; + } + + toggleCollapse = () => { + this.setState(prevState => ({ + collapsed: !prevState.collapsed + })); + }; + + isLastEntry = (array: Plugin[], index: number) => { + return index === array.length - 1; + }; + + isLengthOdd = (array: Plugin[]) => { + return array.length % 2 !== 0; + }; + + isFullSize = (array: Plugin[], index: number) => { + return this.isLastEntry(array, index) && this.isLengthOdd(array); + }; + + render() { + const { group, classes } = this.props; + const { collapsed } = this.state; + + const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; + let content = null; + if (!collapsed) { + content = group.plugins.map((plugin, index) => { + const fullColumnWidth = this.isFullSize(group.plugins, index); + return ( + <PluginEntry + plugin={plugin} + fullColumnWidth={fullColumnWidth} + key={index} + /> + ); + }); + } + return ( + <div className={classes.pluginGroup}> + <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)(PluginGroupEntry); diff --git a/scm-ui/src/admin/plugins/components/PluginsList.js b/scm-ui/src/admin/plugins/components/PluginsList.js index d4c99be888..01f64afe78 100644 --- a/scm-ui/src/admin/plugins/components/PluginsList.js +++ b/scm-ui/src/admin/plugins/components/PluginsList.js @@ -1,24 +1,26 @@ //@flow import React from "react"; -import type { Plugins } from "@scm-manager/ui-types"; +import type { Plugin } from "@scm-manager/ui-types"; +import PluginGroupEntry from "../components/PluginGroupEntry"; +import groupByCategory from "./groupByCategory"; type Props = { - plugins: Plugins[] + plugins: Plugin[] }; -class RepositoryList extends React.Component<Props> { +class PluginList extends React.Component<Props> { render() { const { plugins } = this.props; - const groups = groupByNamespace(plugins); + const groups = groupByCategory(plugins); return ( <div className="content"> {groups.map(group => { - return <PluginEntry group={group} key={group.name} />; + return <PluginGroupEntry group={group} key={group.name} />; })} </div> ); } } -export default RepositoryList; +export default PluginList; diff --git a/scm-ui/src/admin/plugins/components/groupByCategory.js b/scm-ui/src/admin/plugins/components/groupByCategory.js new file mode 100644 index 0000000000..1c542d45e3 --- /dev/null +++ b/scm-ui/src/admin/plugins/components/groupByCategory.js @@ -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; +} diff --git a/scm-ui/src/admin/roles/containers/PluginsList.js b/scm-ui/src/admin/roles/containers/PluginsList.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 44c3aaae62..c9d423a9e7 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -172,6 +172,9 @@ ul.is-separated { height: calc(120px - 1.5rem); width: calc(50% - 3rem); } + .overlay-half-column.is-plugin-page { + width: calc(37.5% - 1.5rem); + } } .column.is-full { .overlay-full-column { @@ -179,6 +182,9 @@ ul.is-separated { height: calc(120px - 0.5rem); width: calc(100% - 1.5rem); } + .overlay-full-column.is-plugin-page { + width: calc(75% - 1.5rem); + } } @media screen and (max-width: 768px) { .column.is-half { @@ -188,12 +194,14 @@ ul.is-separated { margin-right: 0; } - .overlay-half-column { - position: absolute; - height: calc(120px - 0.5rem); + .overlay-half-column, + .overlay-half-column.is-plugin-page { width: calc(100% - 1.5rem); } } + .column.is-full .overlay-full-column.is-plugin-page { + width: calc(100% - 1.5rem); + } } } From 0175dc496ecd7352c9342d52dc09a66699da644c Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 4 Jul 2019 17:32:12 +0200 Subject: [PATCH 08/30] implemented pluginCenter --- .../plugins/containers/PluginsOverview.js | 44 ++- scm-ui/src/admin/plugins/modules/plugins.js | 197 ++++++++++ .../src/admin/plugins/modules/plugins.test.js | 370 ++++++++++++++++++ 3 files changed, 594 insertions(+), 17 deletions(-) create mode 100644 scm-ui/src/admin/plugins/modules/plugins.js create mode 100644 scm-ui/src/admin/plugins/modules/plugins.test.js diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js index 391d205a99..982410c0c5 100644 --- a/scm-ui/src/admin/plugins/containers/PluginsOverview.js +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -1,9 +1,16 @@ // @flow import React from "react"; -import {connect} from "react-redux"; +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, LinkPaginator, Notification } from "@scm-manager/ui-components"; +import { + Loading, + Title, + Subtitle, + Notification, + ErrorNotification +} from "@scm-manager/ui-components"; import { fetchPluginsByLink, getFetchPluginsFailure, @@ -17,7 +24,6 @@ type Props = { loading: boolean, error: Error, collection: PluginCollection, - page: number, baseUrl: string, installed: boolean, pluginsLink: string, @@ -36,9 +42,13 @@ class PluginsOverview extends React.Component<Props> { } render() { - const { loading, installed, t } = this.props; + const { loading, error, collection, installed, t } = this.props; - if (loading) { + if (error) { + return <ErrorNotification error={error} />; + } + + if (!collection || loading) { return <Loading />; } @@ -58,27 +68,21 @@ class PluginsOverview extends React.Component<Props> { } renderPluginsList() { - const { collection, page, t } = this.props; + const { collection, t } = this.props; if (collection._embedded && collection._embedded.plugins.length > 0) { - return ( - <> - <PluginsList plugins={collection._embedded.plugins} /> - <LinkPaginator collection={collection} page={page} /> - </> - ); + return <PluginsList plugins={collection._embedded.plugins} />; } - return ( - <Notification type="info">{t("plugins.noPlugins")}</Notification> - ); + return <Notification type="info">{t("plugins.noPlugins")}</Notification>; } } -const mapStateToProps = (state) => { +const mapStateToProps = state => { const collection = getPluginCollection(state); const loading = isFetchPluginsPending(state); const error = getFetchPluginsFailure(state); const pluginsLink = getUiPluginsLink(state); + return { collection, loading, @@ -95,4 +99,10 @@ const mapDispatchToProps = dispatch => { }; }; -export default connect(mapStateToProps, mapDispatchToProps)(translate("admin")(PluginsOverview)); +export default compose( + translate("admin"), + connect( + mapStateToProps, + mapDispatchToProps + ) +)(PluginsOverview); diff --git a/scm-ui/src/admin/plugins/modules/plugins.js b/scm-ui/src/admin/plugins/modules/plugins.js new file mode 100644 index 0000000000..25f8f7a53a --- /dev/null +++ b/scm-ui/src/admin/plugins/modules/plugins.js @@ -0,0 +1,197 @@ +// @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: + const t = normalizeByName(action.payload); + return t; + 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.name]); + } + 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); +} + +export function getPermissionsLink(state: Object, name: string) { + const plugin = getPlugin(state, name); + return plugin && plugin._links ? plugin._links.permissions.href : undefined; +} diff --git a/scm-ui/src/admin/plugins/modules/plugins.test.js b/scm-ui/src/admin/plugins/modules/plugins.test.js new file mode 100644 index 0000000000..dba52af04b --- /dev/null +++ b/scm-ui/src/admin/plugins/modules/plugins.test.js @@ -0,0 +1,370 @@ +// @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, + getPermissionsLink +} from "./plugins"; +import type { + Plugin, + PluginCollection, + PluginGroup +} 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 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: pluginCollection, + 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 permissions link", () => { + const state = { + plugins: { + byNames: { + "scm-groupmanager-plugin": groupManagerPlugin + } + } + }; + + const link = getPermissionsLink(state, "scm-groupmanager-plugin"); + expect(link).toEqual( + "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin/permissions/" + ); + }); + + 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 + ); + }); +}); From bdda8fe7e111152597d1aaff393710f4ea695ef4 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Thu, 4 Jul 2019 17:35:00 +0200 Subject: [PATCH 09/30] added todo --- .../main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 572feafcdf..26f85fde66 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -29,7 +29,7 @@ public class UIPluginDtoMapper { UIPluginDto dto = new UIPluginDto(); dto.setName(plugin.getPlugin().getInformation().getName()); dto.setBundles(getScriptResources(plugin)); - dto.setType("42"); + dto.setType("42"); //TODO: add plugin category dto.setVersion(plugin.getPlugin().getInformation().getVersion()); dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); dto.setDescription(plugin.getPlugin().getInformation().getDescription()); From b9f9b803367a78ab3216e3091f916f520b7de9f7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 8 Jul 2019 14:42:12 +0200 Subject: [PATCH 10/30] register new reducer --- scm-ui/src/createReduxStore.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index a05799eda7..4b34c78df7 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -18,6 +18,7 @@ import config from "./admin/modules/config"; import roles from "./admin/roles/modules/roles"; import namespaceStrategies from "./admin/modules/namespaceStrategies"; import indexResources from "./modules/indexResource"; +import plugins from "./admin/plugins/modules/plugins"; import type { BrowserHistory } from "history/createBrowserHistory"; import branches from "./repos/branches/modules/branches"; @@ -42,7 +43,8 @@ function createReduxStore(history: BrowserHistory) { config, roles, sources, - namespaceStrategies + namespaceStrategies, + plugins }); return createStore( From 35aa8d71c314075a54471c8ce6c392253b8886b2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 8 Jul 2019 14:43:08 +0200 Subject: [PATCH 11/30] fix Selector --- scm-ui/src/admin/plugins/modules/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/admin/plugins/modules/plugins.js b/scm-ui/src/admin/plugins/modules/plugins.js index 25f8f7a53a..6e94648319 100644 --- a/scm-ui/src/admin/plugins/modules/plugins.js +++ b/scm-ui/src/admin/plugins/modules/plugins.js @@ -158,7 +158,7 @@ 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.name]); + plugins.push(state.plugins.byNames[pluginName]); } return { ...state.plugins.list, From 4e2e742d10785c66099337c0a6369e86c477e233 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 8 Jul 2019 14:43:43 +0200 Subject: [PATCH 12/30] map Category to Frontend --- .../src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java | 4 ++-- .../java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java index ec77afa0ea..cb579cee01 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java @@ -3,10 +3,10 @@ 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 +@Getter +@Setter public class UIPluginDto extends HalRepresentation { private String name; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 26f85fde66..78f6e06b31 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -29,7 +29,7 @@ public class UIPluginDtoMapper { UIPluginDto dto = new UIPluginDto(); dto.setName(plugin.getPlugin().getInformation().getName()); dto.setBundles(getScriptResources(plugin)); - dto.setType("42"); //TODO: add plugin category + dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "No Category defined"); dto.setVersion(plugin.getPlugin().getInformation().getVersion()); dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); dto.setDescription(plugin.getPlugin().getInformation().getDescription()); From 7e418fdde958cac3cb34bacd377b23c00b2af847 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 8 Jul 2019 15:01:27 +0200 Subject: [PATCH 13/30] add Permissions for Plugins --- scm-core/src/main/java/sonia/scm/plugin/Plugin.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java index e8fd166e78..add995a21a 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java @@ -35,6 +35,7 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- +import com.github.sdorra.ssp.StaticPermissions; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; @@ -52,6 +53,7 @@ import java.util.Set; * * @author Sebastian Sdorra */ +@StaticPermissions( value = "plugin", globalPermissions = {"install", "remove"}, permissions = {} ) @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public final class Plugin extends ScmModule From dffe0d07f268656e69ba3076da20eea85bd7d430 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 8 Jul 2019 15:16:31 +0200 Subject: [PATCH 14/30] deactivate available plugins in navigation --- scm-ui/src/admin/containers/Admin.js | 9 +++++---- .../sonia/scm/api/v2/resources/UIPluginDtoMapper.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/admin/containers/Admin.js b/scm-ui/src/admin/containers/Admin.js index b00cc9163c..b9dbe8be1c 100644 --- a/scm-ui/src/admin/containers/Admin.js +++ b/scm-ui/src/admin/containers/Admin.js @@ -145,10 +145,11 @@ class Admin extends React.Component<Props> { to={`${url}/plugins/installed/`} label={t("plugins.menu.installedNavLink")} /> - <NavLink - to={`${url}/plugins/available/`} - label={t("plugins.menu.availableNavLink")} - /> + {/* Activate this again after available plugins page is created */} + {/*<NavLink*/} + {/* to={`${url}/plugins/available/`}*/} + {/* label={t("plugins.menu.availableNavLink")}*/} + {/*/>*/} </SubNavigation> <NavLink to={`${url}/roles/`} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 78f6e06b31..46e4a8b4ac 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -29,7 +29,7 @@ public class UIPluginDtoMapper { UIPluginDto dto = new UIPluginDto(); dto.setName(plugin.getPlugin().getInformation().getName()); dto.setBundles(getScriptResources(plugin)); - dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "No Category defined"); + dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Sonstige/Miscellaneous"); dto.setVersion(plugin.getPlugin().getInformation().getVersion()); dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); dto.setDescription(plugin.getPlugin().getInformation().getDescription()); From 9b1867862f8824aec74c97c502d94d3ad5d1ff9a Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Mon, 8 Jul 2019 16:08:08 +0200 Subject: [PATCH 15/30] fix build error / fix Tests --- .../main/java/sonia/scm/plugin/Plugin.java | 2 -- .../src/admin/plugins/modules/plugins.test.js | 34 ++++++++----------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java index add995a21a..e8fd166e78 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/Plugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/Plugin.java @@ -35,7 +35,6 @@ package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- -import com.github.sdorra.ssp.StaticPermissions; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; @@ -53,7 +52,6 @@ import java.util.Set; * * @author Sebastian Sdorra */ -@StaticPermissions( value = "plugin", globalPermissions = {"install", "remove"}, permissions = {} ) @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public final class Plugin extends ScmModule diff --git a/scm-ui/src/admin/plugins/modules/plugins.test.js b/scm-ui/src/admin/plugins/modules/plugins.test.js index dba52af04b..20b7c10590 100644 --- a/scm-ui/src/admin/plugins/modules/plugins.test.js +++ b/scm-ui/src/admin/plugins/modules/plugins.test.js @@ -21,13 +21,11 @@ import reducer, { fetchPluginSuccess, getPlugin, isFetchPluginPending, - getFetchPluginFailure, - getPermissionsLink + getFetchPluginFailure } from "./plugins"; import type { Plugin, - PluginCollection, - PluginGroup + PluginCollection } from "@scm-manager/ui-types"; const groupManagerPlugin: Plugin = { @@ -75,6 +73,17 @@ const branchwpPlugin: 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: { @@ -267,7 +276,7 @@ describe("plugins selectors", () => { it("should return the plugins collection", () => { const state = { plugins: { - list: pluginCollection, + list: pluginCollectionWithNames, byNames: { "scm-groupmanager-plugin": groupManagerPlugin, "scm-script-plugin": scriptPlugin, @@ -319,21 +328,6 @@ describe("plugins selectors", () => { expect(plugin).toEqual(groupManagerPlugin); }); - it("should return permissions link", () => { - const state = { - plugins: { - byNames: { - "scm-groupmanager-plugin": groupManagerPlugin - } - } - }; - - const link = getPermissionsLink(state, "scm-groupmanager-plugin"); - expect(link).toEqual( - "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin/permissions/" - ); - }); - it("should return true, when fetch plugin is pending", () => { const state = { pending: { From f2fb17d9b5eec1743705d34712e82fb8b9cd217b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Tue, 9 Jul 2019 13:29:25 +0200 Subject: [PATCH 16/30] use same components for plugin and repository overview Created CardColumn and CardColumnGroup which encapsulate the layout for the two column card layout and use them for repository and plugin overview. --- .../packages/ui-components/src/CardColumn.js | 87 +++++++++++++++ .../ui-components/src/CardColumnGroup.js | 103 ++++++++++++++++++ .../packages/ui-components/src/index.js | 2 + .../admin/plugins/components/PluginEntry.js | 98 +++++------------ .../plugins/components/PluginGroupEntry.js | 95 ++-------------- .../repos/components/list/RepositoryEntry.js | 103 ++++++------------ .../components/list/RepositoryGroupEntry.js | 91 ++-------------- scm-ui/styles/scm.scss | 23 ++-- 8 files changed, 288 insertions(+), 314 deletions(-) create mode 100644 scm-ui-components/packages/ui-components/src/CardColumn.js create mode 100644 scm-ui-components/packages/ui-components/src/CardColumnGroup.js diff --git a/scm-ui-components/packages/ui-components/src/CardColumn.js b/scm-ui-components/packages/ui-components/src/CardColumn.js new file mode 100644 index 0000000000..e1eb65255a --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/CardColumn.js @@ -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); diff --git a/scm-ui-components/packages/ui-components/src/CardColumnGroup.js b/scm-ui-components/packages/ui-components/src/CardColumnGroup.js new file mode 100644 index 0000000000..b72bd97dd7 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/CardColumnGroup.js @@ -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); diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index ae6fd6f875..f6d83d6d08 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -32,6 +32,8 @@ export { default as MarkdownView } from "./MarkdownView"; export { default as SyntaxHighlighter } from "./SyntaxHighlighter"; export { default as ErrorBoundary } from "./ErrorBoundary"; 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 * from "./errors"; diff --git a/scm-ui/src/admin/plugins/components/PluginEntry.js b/scm-ui/src/admin/plugins/components/PluginEntry.js index b12c8dd1e1..a8cdaad915 100644 --- a/scm-ui/src/admin/plugins/components/PluginEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginEntry.js @@ -1,86 +1,44 @@ //@flow import React from "react"; -import { Link } from "react-router-dom"; -import injectSheet from "react-jss"; -import classNames from "classnames"; import type { Plugin } from "@scm-manager/ui-types"; +import { CardColumn } from "@scm-manager/ui-components"; import PluginAvatar from "./PluginAvatar"; -const styles = { - inner: { - position: "relative", - pointerEvents: "none", - zIndex: 1 - }, - centerImage: { - marginTop: "0.8em", - marginLeft: "1em !important" - }, - marginBottom: { - marginBottom: "0.75rem !important" - } -}; - type Props = { - plugin: Plugin, - fullColumnWidth?: boolean, - - // context props - classes: any + 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, classes, fullColumnWidth } = this.props; - const halfColumn = fullColumnWidth ? "is-full" : "is-half"; - const overlayLinkClass = fullColumnWidth - ? "overlay-full-column" - : "overlay-half-column"; + 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 ( - <div - className={classNames( - "box", - "box-link-shadow", - "column", - "is-clipped", - halfColumn - )} - > - <Link - className={classNames(overlayLinkClass, "is-plugin-page")} - to="#" - /> - <article className={classNames("media", classes.inner)}> - <figure className={classNames(classes.centerImage, "media-left")}> - <PluginAvatar plugin={plugin} /> - </figure> - <div className={classNames("media-content", "text-box")}> - <div className="content"> - <nav - className={classNames( - "level", - "is-mobile", - classes.marginBottom - )} - > - <div className="level-left"> - <strong>{plugin.name}</strong> - </div> - <div className="level-right is-hidden-mobile"> - {plugin.version} - </div> - </nav> - <p className="shorten-text is-marginless">{plugin.description}</p> - <p> - <small>{plugin.author}</small> - </p> - </div> - </div> - </article> - </div> + <CardColumn + link="#" + avatar={avatar} + title={plugin.name} + description={plugin.description} + footerLeft={footerLeft} + footerRight={footerRight} + /> ); } } -export default injectSheet(styles)(PluginEntry); +export default PluginEntry; diff --git a/scm-ui/src/admin/plugins/components/PluginGroupEntry.js b/scm-ui/src/admin/plugins/components/PluginGroupEntry.js index 87076f6eaf..44046eb6ab 100644 --- a/scm-ui/src/admin/plugins/components/PluginGroupEntry.js +++ b/scm-ui/src/admin/plugins/components/PluginGroupEntry.js @@ -1,96 +1,21 @@ //@flow import React from "react"; -import injectSheet from "react-jss"; -import classNames from "classnames"; -import type { PluginGroup, Plugin } from "@scm-manager/ui-types"; +import { CardColumnGroup } from "@scm-manager/ui-components"; +import type { PluginGroup } from "@scm-manager/ui-types"; import PluginEntry from "./PluginEntry"; -const styles = { - pointer: { - cursor: "pointer", - fontSize: "1.5rem" - }, - pluginGroup: { - marginBottom: "1em" - }, - wrapper: { - padding: "0 0.75rem" - }, - clearfix: { - clear: "both" - } -}; - type Props = { - group: PluginGroup, - - // context props - classes: any + group: PluginGroup }; -type State = { - collapsed: boolean -}; - -class PluginGroupEntry extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - this.state = { - collapsed: false - }; - } - - toggleCollapse = () => { - this.setState(prevState => ({ - collapsed: !prevState.collapsed - })); - }; - - isLastEntry = (array: Plugin[], index: number) => { - return index === array.length - 1; - }; - - isLengthOdd = (array: Plugin[]) => { - return array.length % 2 !== 0; - }; - - isFullSize = (array: Plugin[], index: number) => { - return this.isLastEntry(array, index) && this.isLengthOdd(array); - }; - +class PluginGroupEntry extends React.Component<Props> { render() { - const { group, classes } = this.props; - const { collapsed } = this.state; - - const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; - let content = null; - if (!collapsed) { - content = group.plugins.map((plugin, index) => { - const fullColumnWidth = this.isFullSize(group.plugins, index); - return ( - <PluginEntry - plugin={plugin} - fullColumnWidth={fullColumnWidth} - key={index} - /> - ); - }); - } - return ( - <div className={classes.pluginGroup}> - <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> - ); + 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 injectSheet(styles)(PluginGroupEntry); +export default PluginGroupEntry; diff --git a/scm-ui/src/repos/components/list/RepositoryEntry.js b/scm-ui/src/repos/components/list/RepositoryEntry.js index eb3b3d95f6..471f900aa1 100644 --- a/scm-ui/src/repos/components/list/RepositoryEntry.js +++ b/scm-ui/src/repos/components/list/RepositoryEntry.js @@ -1,33 +1,12 @@ //@flow import React from "react"; -import { Link } from "react-router-dom"; -import injectSheet from "react-jss"; 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 classNames from "classnames"; 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 = { - repository: Repository, - fullColumnWidth?: boolean, - // context props - classes: any + repository: Repository }; class RepositoryEntry extends React.Component<Props> { @@ -83,53 +62,41 @@ class RepositoryEntry extends React.Component<Props> { return null; }; - render() { - 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"; + createFooterLeft = (repository: Repository, repositoryLink: string) => { 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.renderChangesetsLink(repository, repositoryLink)} - {this.renderSourcesLink(repository, repositoryLink)} - {this.renderModifyLink(repository, repositoryLink)} - </div> - <div className="level-right is-hidden-mobile"> - <small className="level-item"> - <DateFromNow date={repository.creationDate} /> - </small> - </div> - </nav> - </div> - </article> - </div> + <> + {this.renderBranchesLink(repository, repositoryLink)} + {this.renderChangesetsLink(repository, repositoryLink)} + {this.renderSourcesLink(repository, repositoryLink)} + {this.renderModifyLink(repository, repositoryLink)} + </> + ); + }; + + createFooterRight = (repository: Repository) => { + return ( + <small className="level-item"> + <DateFromNow date={repository.creationDate} /> + </small> + ); + }; + + render() { + 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; diff --git a/scm-ui/src/repos/components/list/RepositoryGroupEntry.js b/scm-ui/src/repos/components/list/RepositoryGroupEntry.js index 98e7925150..8f7e25dcbe 100644 --- a/scm-ui/src/repos/components/list/RepositoryGroupEntry.js +++ b/scm-ui/src/repos/components/list/RepositoryGroupEntry.js @@ -1,92 +1,21 @@ //@flow import React from "react"; -import type { RepositoryGroup, Repository } from "@scm-manager/ui-types"; -import injectSheet from "react-jss"; -import classNames from "classnames"; +import { CardColumnGroup } from "@scm-manager/ui-components"; +import type { RepositoryGroup } from "@scm-manager/ui-types"; 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 = { - group: RepositoryGroup, - - // context props - classes: any + group: RepositoryGroup }; -type State = { - 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); - }; - +class RepositoryGroupEntry extends React.Component<Props> { render() { - const { group, classes } = this.props; - const { collapsed } = this.state; - - 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 ( - <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> - ); + const { group } = this.props; + const entries = group.repositories.map((repository, index) => { + return <RepositoryEntry repository={repository} key={index} />; + }); + return <CardColumnGroup name={group.name} elements={entries} />; } } -export default injectSheet(styles)(RepositoryGroupEntry); +export default RepositoryGroupEntry; diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index c9d423a9e7..222090cef4 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -159,30 +159,33 @@ ul.is-separated { // multiline Columns .columns.is-multiline { + .column.is-half { + height: 120px; width: calc(50% - 0.75rem); - max-height: 120px; &:nth-child(odd) { margin-right: 1.5rem; } - .overlay-half-column { + .overlay-column { position: absolute; - height: calc(120px - 1.5rem); + height: 120px; width: calc(50% - 3rem); } - .overlay-half-column.is-plugin-page { + .overlay-column.is-plugin-page { width: calc(37.5% - 1.5rem); } } .column.is-full { - .overlay-full-column { + height: 120px; + + .overlay-column { position: absolute; - height: calc(120px - 0.5rem); + height: 120px; width: calc(100% - 1.5rem); } - .overlay-full-column.is-plugin-page { + .overlay-column.is-plugin-page { width: calc(75% - 1.5rem); } } @@ -194,12 +197,12 @@ ul.is-separated { margin-right: 0; } - .overlay-half-column, - .overlay-half-column.is-plugin-page { + .overlay-column, + .overlay-column.is-plugin-page { width: calc(100% - 1.5rem); } } - .column.is-full .overlay-full-column.is-plugin-page { + .column.is-full .overlay-column.is-plugin-page { width: calc(100% - 1.5rem); } } From e4e49a033d49f78c5e80e4aac5341c851c703130 Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Tue, 9 Jul 2019 16:12:02 +0200 Subject: [PATCH 17/30] fixed link overlay in pluginsList --- .../admin/plugins/components/PluginsList.js | 2 +- scm-ui/styles/scm.scss | 49 +++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/scm-ui/src/admin/plugins/components/PluginsList.js b/scm-ui/src/admin/plugins/components/PluginsList.js index 01f64afe78..e04d78d46e 100644 --- a/scm-ui/src/admin/plugins/components/PluginsList.js +++ b/scm-ui/src/admin/plugins/components/PluginsList.js @@ -14,7 +14,7 @@ class PluginList extends React.Component<Props> { const groups = groupByCategory(plugins); return ( - <div className="content"> + <div className="content is-plugin-page"> {groups.map(group => { return <PluginGroupEntry group={group} key={group.name} />; })} diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index 222090cef4..8378bb23e9 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -159,9 +159,16 @@ ul.is-separated { // multiline Columns .columns.is-multiline { + .column { + height: 120px; + + .overlay-column { + position: absolute; + height: calc(120px - 1.5rem); + } + } .column.is-half { - height: 120px; width: calc(50% - 0.75rem); &:nth-child(odd) { @@ -169,26 +176,14 @@ ul.is-separated { } .overlay-column { - position: absolute; - height: 120px; width: calc(50% - 3rem); } - .overlay-column.is-plugin-page { - width: calc(37.5% - 1.5rem); - } } - .column.is-full { - height: 120px; - .overlay-column { - position: absolute; - height: 120px; - width: calc(100% - 1.5rem); - } - .overlay-column.is-plugin-page { - width: calc(75% - 1.5rem); - } + .column.is-full .overlay-column { + width: calc(100% - 1.5rem); } + @media screen and (max-width: 768px) { .column.is-half { width: 100%; @@ -197,13 +192,27 @@ ul.is-separated { margin-right: 0; } - .overlay-column, - .overlay-column.is-plugin-page { + .overlay-column { width: calc(100% - 1.5rem); } } - .column.is-full .overlay-column.is-plugin-page { - width: calc(100% - 1.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); + } } } } From d7d601a82f7d9505cffc8d2a43d7e4af1a8bb68e Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Tue, 9 Jul 2019 16:22:04 +0200 Subject: [PATCH 18/30] removed unused function --- scm-ui/src/admin/plugins/modules/plugins.js | 8 +---- .../src/admin/plugins/modules/plugins.test.js | 29 ++++++------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/scm-ui/src/admin/plugins/modules/plugins.js b/scm-ui/src/admin/plugins/modules/plugins.js index 6e94648319..3f475c68b6 100644 --- a/scm-ui/src/admin/plugins/modules/plugins.js +++ b/scm-ui/src/admin/plugins/modules/plugins.js @@ -144,8 +144,7 @@ export default function reducer( switch (action.type) { case FETCH_PLUGINS_SUCCESS: - const t = normalizeByName(action.payload); - return t; + return normalizeByName(action.payload); case FETCH_PLUGIN_SUCCESS: return reducerByNames(state, action.payload); default: @@ -190,8 +189,3 @@ export function isFetchPluginPending(state: Object, name: string) { export function getFetchPluginFailure(state: Object, name: string) { return getFailure(state, FETCH_PLUGIN, name); } - -export function getPermissionsLink(state: Object, name: string) { - const plugin = getPlugin(state, name); - return plugin && plugin._links ? plugin._links.permissions.href : undefined; -} diff --git a/scm-ui/src/admin/plugins/modules/plugins.test.js b/scm-ui/src/admin/plugins/modules/plugins.test.js index 20b7c10590..d577b20996 100644 --- a/scm-ui/src/admin/plugins/modules/plugins.test.js +++ b/scm-ui/src/admin/plugins/modules/plugins.test.js @@ -23,10 +23,7 @@ import reducer, { isFetchPluginPending, getFetchPluginFailure } from "./plugins"; -import type { - Plugin, - PluginCollection -} from "@scm-manager/ui-types"; +import type { Plugin, PluginCollection } from "@scm-manager/ui-types"; const groupManagerPlugin: Plugin = { name: "scm-groupmanager-plugin", @@ -37,8 +34,7 @@ const groupManagerPlugin: Plugin = { description: "Notify a remote webserver whenever a plugin is pushed to.", _links: { self: { - href: - "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin" + href: "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin" } } }; @@ -52,8 +48,7 @@ const scriptPlugin: Plugin = { description: "Script support for scm-manager.", _links: { self: { - href: - "http://localhost:8081/api/v2/ui/plugins/scm-script-plugin" + href: "http://localhost:8081/api/v2/ui/plugins/scm-script-plugin" } } }; @@ -67,8 +62,7 @@ const branchwpPlugin: Plugin = { description: "This plugin adds branch write protection for plugins.", _links: { self: { - href: - "http://localhost:8081/api/v2/ui/plugins/scm-branchwp-plugin" + href: "http://localhost:8081/api/v2/ui/plugins/scm-branchwp-plugin" } } }; @@ -166,12 +160,9 @@ describe("plugins fetch", () => { }); 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 - } - ); + fetchMock.getOnce(PLUGINS_URL + "/scm-groupmanager-plugin", { + status: 500 + }); const store = mockStore({}); return store @@ -331,8 +322,7 @@ describe("plugins selectors", () => { it("should return true, when fetch plugin is pending", () => { const state = { pending: { - [FETCH_PLUGIN + - "/scm-groupmanager-plugin"]: true + [FETCH_PLUGIN + "/scm-groupmanager-plugin"]: true } }; expect(isFetchPluginPending(state, "scm-groupmanager-plugin")).toEqual( @@ -347,8 +337,7 @@ describe("plugins selectors", () => { it("should return error when fetch plugin did fail", () => { const state = { failure: { - [FETCH_PLUGIN + - "/scm-groupmanager-plugin"]: error + [FETCH_PLUGIN + "/scm-groupmanager-plugin"]: error } }; expect(getFetchPluginFailure(state, "scm-groupmanager-plugin")).toEqual( From 4ef767c4801ac50691f8b4bebff2c9b0832e89dd Mon Sep 17 00:00:00 2001 From: Florian Scholdei <florian.scholdei@cloudogu.com> Date: Tue, 9 Jul 2019 16:24:43 +0200 Subject: [PATCH 19/30] used only english translation as miscellaneous category for plugins --- .../main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index 46e4a8b4ac..a586fbcbff 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -29,7 +29,7 @@ public class UIPluginDtoMapper { UIPluginDto dto = new UIPluginDto(); dto.setName(plugin.getPlugin().getInformation().getName()); dto.setBundles(getScriptResources(plugin)); - dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Sonstige/Miscellaneous"); + dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Miscellaneous"); dto.setVersion(plugin.getPlugin().getInformation().getVersion()); dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); dto.setDescription(plugin.getPlugin().getInformation().getDescription()); From 64d3153347c57df2592600a303987146bc165bae Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Tue, 9 Jul 2019 17:04:29 +0200 Subject: [PATCH 20/30] create new Endpoints and Resources for Plugins --- .../main/java/sonia/scm/web/VndMediaType.java | 2 + .../sonia/scm/api/v2/resources/PluginDto.java | 26 ++++++ .../resources/PluginDtoCollectionMapper.java | 45 ++++++++++ .../scm/api/v2/resources/PluginDtoMapper.java | 62 ++++++++++++++ .../scm/api/v2/resources/PluginResource.java | 84 +++++++++++++++++++ .../api/v2/resources/PluginRootResource.java | 21 +++++ .../scm/api/v2/resources/ResourceLinks.java | 32 +++++++ 7 files changed, 272 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index c9696b0641..24250b26ba 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -37,6 +37,8 @@ public class VndMediaType { 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 = 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_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX; @SuppressWarnings("squid:S2068") diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java new file mode 100644 index 0000000000..0d686b237b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java @@ -0,0 +1,26 @@ +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; + + @Override + protected HalRepresentation add(Links links) { + return super.add(links); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java new file mode 100644 index 0000000000..72178e94f3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java @@ -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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java new file mode 100644 index 0000000000..c910802cac --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -0,0 +1,62 @@ +package sonia.scm.api.v2.resources; + +import com.google.common.base.Strings; +import de.otto.edison.hal.Links; +import sonia.scm.plugin.PluginWrapper; +import sonia.scm.util.HttpUtil; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import static de.otto.edison.hal.Links.linkingTo; + +public class PluginDtoMapper { + + private final ResourceLinks resourceLinks; + private final HttpServletRequest request; + + @Inject + public PluginDtoMapper(ResourceLinks resourceLinks, HttpServletRequest request) { + this.resourceLinks = resourceLinks; + this.request = request; + } + + public PluginDto map(PluginWrapper plugin) { + PluginDto pluginDto = new PluginDto(); + pluginDto.setName(plugin.getPlugin().getInformation().getName()); + pluginDto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Sonstige/Miscellaneous"); + pluginDto.setVersion(plugin.getPlugin().getInformation().getVersion()); + pluginDto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); + pluginDto.setDescription(plugin.getPlugin().getInformation().getDescription()); + + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.uiPlugin() + .self(plugin.getId())); + + pluginDto.add(linksBuilder.build()); + + return pluginDto; + } + + private Set<String> getScriptResources(PluginWrapper wrapper) { + Set<String> scriptResources = wrapper.getPlugin().getResources().getScriptResources(); + if (scriptResources != null) { + return scriptResources.stream() + .map(this::addContextPath) + .collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + private String addContextPath(String resource) { + String ctxPath = request.getContextPath(); + if (Strings.isNullOrEmpty(ctxPath)) { + return resource; + } + return HttpUtil.append(ctxPath, resource); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java new file mode 100644 index 0000000000..fc49a51513 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java @@ -0,0 +1,84 @@ +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.PluginLoader; +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.List; +import java.util.Optional; +import java.util.stream.Collectors; + +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() { + List<PluginWrapper> plugins = pluginLoader.getInstalledPlugins() + .stream() + .collect(Collectors.toList()); + + 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) { + Optional<PluginDto> pluginDto = pluginLoader.getInstalledPlugins() + .stream() + .filter(plugin -> id.equals(plugin.getId())) + .map(mapper::map) + .findFirst(); + + if (pluginDto.isPresent()) { + return Response.ok(pluginDto.get()).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java new file mode 100644 index 0000000000..e9b0f0a997 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java @@ -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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 5e7e89a2e6..e9d47f2346 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -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, UIRootResource.class, UIPluginResource.class); + } + + String self() { + return pluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href(); + } + } + public AuthenticationLinks authentication() { return new AuthenticationLinks(scmPathInfoStore.get()); } From f008faabcb22f4176fc92cc4ab050009fc28176b Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 08:11:19 +0200 Subject: [PATCH 21/30] add IndexEnricher --- .../java/sonia/scm/api/v2/resources/IndexDtoGenerator.java | 5 +++++ .../main/java/sonia/scm/api/v2/resources/ResourceLinks.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 00982a1609..cccef51422 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -8,6 +8,7 @@ import org.apache.shiro.SecurityUtils; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.group.GroupPermissions; +import sonia.scm.plugin.PluginPermissions; import sonia.scm.repository.RepositoryRolePermissions; import sonia.scm.security.PermissionPermissions; import sonia.scm.user.UserPermissions; @@ -34,11 +35,15 @@ public class IndexDtoGenerator extends HalAppenderMapper { List<Link> autoCompleteLinks = Lists.newArrayList(); builder.self(resourceLinks.index().self()); builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self())); + if (SecurityUtils.getSubject().isAuthenticated()) { builder.single( link("me", resourceLinks.me().self()), link("logout", resourceLinks.authentication().logout()) ); + if (PluginPermissions.custom(PluginPermissions.ACTION_READ).isPermitted()) { + builder.single(link("plugins", resourceLinks.pluginCollection().self())); + } if (UserPermissions.list().isPermitted()) { builder.single(link("users", resourceLinks.userCollection().self())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index e9d47f2346..1d06659649 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -675,7 +675,7 @@ class ResourceLinks { private final LinkBuilder pluginCollectionLinkBuilder; PluginCollectionLinks(ScmPathInfo pathInfo) { - pluginCollectionLinkBuilder = new LinkBuilder(pathInfo, UIRootResource.class, UIPluginResource.class); + pluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PluginResource.class); } String self() { From 5c473792ef5840f5be68054d294597cc60cd048f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 08:21:05 +0200 Subject: [PATCH 22/30] add PermissionCheck for Plugins --- .../main/java/sonia/scm/api/v2/resources/PluginResource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java index fc49a51513..6da204ea6c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java @@ -4,6 +4,7 @@ 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.PluginLoader; +import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginWrapper; import sonia.scm.web.VndMediaType; @@ -47,7 +48,7 @@ public class PluginResource { List<PluginWrapper> plugins = pluginLoader.getInstalledPlugins() .stream() .collect(Collectors.toList()); - + PluginPermissions.read().check(); return Response.ok(collectionMapper.map(plugins)).build(); } @@ -73,7 +74,7 @@ public class PluginResource { .filter(plugin -> id.equals(plugin.getId())) .map(mapper::map) .findFirst(); - + PluginPermissions.read().check(); if (pluginDto.isPresent()) { return Response.ok(pluginDto.get()).build(); } else { From 4cd9718c773965119fbba78d47c44674e52bcbfa Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 09:18:12 +0200 Subject: [PATCH 23/30] hide Plugins-Tab in Navigation if pluginsLink not available --- scm-ui/src/admin/containers/Admin.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scm-ui/src/admin/containers/Admin.js b/scm-ui/src/admin/containers/Admin.js index b9dbe8be1c..58e37a0a7c 100644 --- a/scm-ui/src/admin/containers/Admin.js +++ b/scm-ui/src/admin/containers/Admin.js @@ -136,6 +136,8 @@ class Admin extends React.Component<Props> { icon="fas fa-info-circle" label={t("admin.menu.informationNavLink")} /> + { + links.plugins && <SubNavigation to={`${url}/plugins/`} icon="fas fa-puzzle-piece" @@ -151,6 +153,7 @@ class Admin extends React.Component<Props> { {/* label={t("plugins.menu.availableNavLink")}*/} {/*/>*/} </SubNavigation> + } <NavLink to={`${url}/roles/`} icon="fas fa-user-shield" From 01280d8d527f43debe49c256c4b8dc5a2c8d102b Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 09:18:50 +0200 Subject: [PATCH 24/30] use right pluginsLink in frontend --- scm-ui/src/admin/plugins/containers/PluginsOverview.js | 4 ++-- scm-ui/src/modules/indexResource.js | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/admin/plugins/containers/PluginsOverview.js b/scm-ui/src/admin/plugins/containers/PluginsOverview.js index 982410c0c5..7a3fc7ec4a 100644 --- a/scm-ui/src/admin/plugins/containers/PluginsOverview.js +++ b/scm-ui/src/admin/plugins/containers/PluginsOverview.js @@ -18,7 +18,7 @@ import { isFetchPluginsPending } from "../modules/plugins"; import PluginsList from "../components/PluginsList"; -import { getUiPluginsLink } from "../../../modules/indexResource"; +import { getPluginsLink } from "../../../modules/indexResource"; type Props = { loading: boolean, @@ -81,7 +81,7 @@ const mapStateToProps = state => { const collection = getPluginCollection(state); const loading = isFetchPluginsPending(state); const error = getFetchPluginsFailure(state); - const pluginsLink = getUiPluginsLink(state); + const pluginsLink = getPluginsLink(state); return { collection, diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index e9b7e67baf..9bfa620674 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -116,6 +116,10 @@ export function getUiPluginsLink(state: Object) { return getLink(state, "uiPlugins"); } +export function getPluginsLink(state: Object) { + return getLink(state, "plugins"); +} + export function getMeLink(state: Object) { return getLink(state, "me"); } From 00ff6dbf0b18307f47b5f8b541b800d49df25644 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 09:47:19 +0200 Subject: [PATCH 25/30] add plugin permissions --- .../src/main/resources/META-INF/scm/permissions.xml | 9 +++++++-- scm-webapp/src/main/resources/locales/de/plugins.json | 10 ++++++++++ scm-webapp/src/main/resources/locales/en/plugins.json | 10 ++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 3598b178bf..a8bc21973b 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -67,7 +67,12 @@ <value>configuration:read,write:*</value> </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> - </permissions> diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 0bdfa39b3e..92b81c5772 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -66,6 +66,16 @@ "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" }, "verbs": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 4255f519ca..3db2ce7c65 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -66,6 +66,16 @@ "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 read and manage all installed and available plugins" + } + }, "unknown": "Unknown permission" }, "verbs": { From 89a066fd38ab044c966ada0a68b127efebcfb862 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 10:30:23 +0200 Subject: [PATCH 26/30] add Translations --- .../src/main/resources/locales/de/plugins.json | 12 ++++++++---- .../src/main/resources/locales/en/plugins.json | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 92b81c5772..1e46b5aeac 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -68,12 +68,16 @@ }, "plugin": { "read": { - "displayName": "Alle Plugins lesen", - "description": "Darf alle installierten und verfügbaren Plugins lesen" + "*": { + "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" + "*": { + "displayName": "Alle Plugins lesen und verwalten", + "description": "Darf alle installierten und verfügbaren Plugins lesen und verwalten" + } } }, "unknown": "Unbekannte Berechtigung" diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 3db2ce7c65..77c1534fed 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -68,12 +68,16 @@ }, "plugin": { "read": { - "displayName": "Read all plugins", - "description": "May see all installed and available plugins" + "*": { + "displayName": "Read all plugins", + "description": "May see all installed and available plugins" + } }, "read,write": { - "displayName": "Read and manage all plugins", - "description": "May read and manage all installed and available plugins" + "*": { + "displayName": "Read and manage all plugins", + "description": "May see and manage all installed and available plugins" + } } }, "unknown": "Unknown permission" From 1283b06e98be99efc4a2293c14d07223d4131b82 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Wed, 10 Jul 2019 10:46:42 +0200 Subject: [PATCH 27/30] cleanup / revert UIPlugin changes --- .../scm/api/v2/resources/PluginDtoMapper.java | 27 +------------------ .../scm/api/v2/resources/UIPluginDto.java | 13 ++++----- .../api/v2/resources/UIPluginDtoMapper.java | 11 +++----- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index c910802cac..fe940e2c8a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -1,15 +1,9 @@ package sonia.scm.api.v2.resources; -import com.google.common.base.Strings; import de.otto.edison.hal.Links; import sonia.scm.plugin.PluginWrapper; -import sonia.scm.util.HttpUtil; - import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; import static de.otto.edison.hal.Links.linkingTo; @@ -27,7 +21,7 @@ public class PluginDtoMapper { public PluginDto map(PluginWrapper plugin) { PluginDto pluginDto = new PluginDto(); pluginDto.setName(plugin.getPlugin().getInformation().getName()); - pluginDto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Sonstige/Miscellaneous"); + 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()); @@ -40,23 +34,4 @@ public class PluginDtoMapper { return pluginDto; } - - private Set<String> getScriptResources(PluginWrapper wrapper) { - Set<String> scriptResources = wrapper.getPlugin().getResources().getScriptResources(); - if (scriptResources != null) { - return scriptResources.stream() - .map(this::addContextPath) - .collect(Collectors.toSet()); - } - return Collections.emptySet(); - } - - private String addContextPath(String resource) { - String ctxPath = request.getContextPath(); - if (Strings.isNullOrEmpty(ctxPath)) { - return resource; - } - return HttpUtil.append(ctxPath, resource); - } - } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java index cb579cee01..dfed9a3612 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDto.java @@ -3,18 +3,19 @@ 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 +@Getter @Setter @NoArgsConstructor public class UIPluginDto extends HalRepresentation { private String name; private Iterable<String> bundles; - private String type; - private String version; - private String author; - private String description; + + public UIPluginDto(String name, Iterable<String> bundles) { + this.name = name; + this.bundles = bundles; + } @Override protected HalRepresentation add(Links links) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java index a586fbcbff..10ae79b5bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UIPluginDtoMapper.java @@ -26,13 +26,10 @@ public class UIPluginDtoMapper { } public UIPluginDto map(PluginWrapper plugin) { - UIPluginDto dto = new UIPluginDto(); - dto.setName(plugin.getPlugin().getInformation().getName()); - dto.setBundles(getScriptResources(plugin)); - dto.setType(plugin.getPlugin().getInformation().getCategory() != null ? plugin.getPlugin().getInformation().getCategory() : "Miscellaneous"); - dto.setVersion(plugin.getPlugin().getInformation().getVersion()); - dto.setAuthor(plugin.getPlugin().getInformation().getAuthor()); - dto.setDescription(plugin.getPlugin().getInformation().getDescription()); + UIPluginDto dto = new UIPluginDto( + plugin.getPlugin().getInformation().getName(), + getScriptResources(plugin) + ); Links.Builder linksBuilder = linkingTo() .self(resourceLinks.uiPlugin() From 77aca0531e016df7776de9fea2e892d95ba2de25 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer <rene.pfeuffer@cloudogu.com> Date: Thu, 11 Jul 2019 13:13:03 +0200 Subject: [PATCH 28/30] Small fixes --- .../scm/api/v2/resources/IndexDtoGenerator.java | 2 +- .../sonia/scm/api/v2/resources/PluginDto.java | 6 ++---- .../scm/api/v2/resources/PluginDtoMapper.java | 17 ++++++----------- .../scm/api/v2/resources/PluginResource.java | 14 ++++++++------ .../main/resources/META-INF/scm/permissions.xml | 5 +++-- .../src/main/resources/locales/de/plugins.json | 12 ++++-------- .../src/main/resources/locales/en/plugins.json | 12 ++++-------- 7 files changed, 28 insertions(+), 40 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index cccef51422..c7b52861dc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -41,7 +41,7 @@ public class IndexDtoGenerator extends HalAppenderMapper { link("me", resourceLinks.me().self()), link("logout", resourceLinks.authentication().logout()) ); - if (PluginPermissions.custom(PluginPermissions.ACTION_READ).isPermitted()) { + if (PluginPermissions.read().isPermitted()) { builder.single(link("plugins", resourceLinks.pluginCollection().self())); } if (UserPermissions.list().isPermitted()) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java index 0d686b237b..d119eca711 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java @@ -18,9 +18,7 @@ public class PluginDto extends HalRepresentation { private String author; private String description; - @Override - protected HalRepresentation add(Links links) { - return super.add(links); + public PluginDto(Links links) { + add(links); } - } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index fe940e2c8a..23ecbe4ad8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -3,35 +3,30 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; import sonia.scm.plugin.PluginWrapper; import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; import static de.otto.edison.hal.Links.linkingTo; public class PluginDtoMapper { private final ResourceLinks resourceLinks; - private final HttpServletRequest request; @Inject - public PluginDtoMapper(ResourceLinks resourceLinks, HttpServletRequest request) { + public PluginDtoMapper(ResourceLinks resourceLinks) { this.resourceLinks = resourceLinks; - this.request = request; } public PluginDto map(PluginWrapper plugin) { - PluginDto pluginDto = new PluginDto(); + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.plugin() + .self(plugin.getId())); + + 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()); - Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.uiPlugin() - .self(plugin.getId())); - - pluginDto.add(linksBuilder.build()); - return pluginDto; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java index 6da204ea6c..3cf0528dd3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java @@ -3,6 +3,7 @@ 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; @@ -14,9 +15,12 @@ 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 java.util.stream.Collectors; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.NotFoundException.notFound; public class PluginResource { @@ -45,10 +49,8 @@ public class PluginResource { @TypeHint(CollectionDto.class) @Produces(VndMediaType.PLUGIN_COLLECTION) public Response getInstalledPlugins() { - List<PluginWrapper> plugins = pluginLoader.getInstalledPlugins() - .stream() - .collect(Collectors.toList()); PluginPermissions.read().check(); + List<PluginWrapper> plugins = new ArrayList<>(pluginLoader.getInstalledPlugins()); return Response.ok(collectionMapper.map(plugins)).build(); } @@ -69,16 +71,16 @@ public class PluginResource { @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.getId())) .map(mapper::map) .findFirst(); - PluginPermissions.read().check(); if (pluginDto.isPresent()) { return Response.ok(pluginDto.get()).build(); } else { - return Response.status(Response.Status.NOT_FOUND).build(); + throw notFound(entity(Plugin.class, id)); } } diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index a8bc21973b..6a718e4d01 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -70,9 +70,10 @@ <value>repositoryRole:read,write</value> </permission> <permission> - <value>plugin:read:*</value> + <value>plugin:read</value> </permission> <permission> - <value>plugin:read,write:*</value> + <value>plugin:read,write</value> </permission> </permissions> + diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 1e46b5aeac..92b81c5772 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -68,16 +68,12 @@ }, "plugin": { "read": { - "*": { - "displayName": "Alle Plugins lesen", - "description": "Darf alle installierten und verfügbaren Plugins lesen" - } + "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" - } + "displayName": "Alle Plugins lesen und verwalten", + "description": "Darf alle installierten und verfügbaren Plugins lesen und verwalten" } }, "unknown": "Unbekannte Berechtigung" diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 77c1534fed..3f70000624 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -68,16 +68,12 @@ }, "plugin": { "read": { - "*": { - "displayName": "Read all plugins", - "description": "May see all installed and available plugins" - } + "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" - } + "displayName": "Read and manage all plugins", + "description": "May see and manage all installed and available plugins" } }, "unknown": "Unknown permission" From 30a89255106ac56affce1484382207f8715d8943 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 11 Jul 2019 13:29:53 +0200 Subject: [PATCH 29/30] remove version from pluginLink --- .../main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java | 2 +- .../main/java/sonia/scm/api/v2/resources/PluginResource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index 23ecbe4ad8..d17ecdae70 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -18,7 +18,7 @@ public class PluginDtoMapper { public PluginDto map(PluginWrapper plugin) { Links.Builder linksBuilder = linkingTo() .self(resourceLinks.plugin() - .self(plugin.getId())); + .self(plugin.getPlugin().getInformation().getId(false))); PluginDto pluginDto = new PluginDto(linksBuilder.build()); pluginDto.setName(plugin.getPlugin().getInformation().getName()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java index 3cf0528dd3..c3b6ea6020 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginResource.java @@ -74,7 +74,7 @@ public class PluginResource { PluginPermissions.read().check(); Optional<PluginDto> pluginDto = pluginLoader.getInstalledPlugins() .stream() - .filter(plugin -> id.equals(plugin.getId())) + .filter(plugin -> id.equals(plugin.getPlugin().getInformation().getId(false))) .map(mapper::map) .findFirst(); if (pluginDto.isPresent()) { From b8f2f9b13af899ed5eaa4af5af1f6ba40f65562d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Date: Thu, 11 Jul 2019 11:31:35 +0000 Subject: [PATCH 30/30] Close branch feature/plugin_center