Include cloudogu plugins to plugin center (#1709)

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-06-25 09:22:53 +02:00
committed by GitHub
parent d9d3547a22
commit 7a3db7ee3f
21 changed files with 188 additions and 104 deletions

View File

@@ -0,0 +1,2 @@
- type: Added
description: Prepare plugin center to show cloudogu plugins ([#1709](https://github.com/scm-manager/scm-manager/pull/1709))

View File

@@ -40,22 +40,32 @@ public class AvailablePluginDescriptor implements PluginDescriptor {
private final Set<String> optionalDependencies; private final Set<String> optionalDependencies;
private final String url; private final String url;
private final String checksum; private final String checksum;
private final String installLink;
/** /**
* @deprecated Use {@link #AvailablePluginDescriptor(PluginInformation, PluginCondition, Set, Set, String, String)} instead * @deprecated Use {@link #AvailablePluginDescriptor(PluginInformation, PluginCondition, Set, Set, String, String, String)} instead
*/ */
@Deprecated @Deprecated
public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, String url, String checksum) { public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, String url, String checksum) {
this(information, condition, dependencies, emptySet(), url, checksum); this(information, condition, dependencies, emptySet(), url, checksum);
} }
/**
* @deprecated Use {@link #AvailablePluginDescriptor(PluginInformation, PluginCondition, Set, Set, String, String, String)} instead
*/
@Deprecated
public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, Set<String> optionalDependencies, String url, String checksum) { public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, Set<String> optionalDependencies, String url, String checksum) {
this(information, condition, dependencies, optionalDependencies, url, checksum, null);
}
public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set<String> dependencies, Set<String> optionalDependencies, String url, String checksum, String installLink) {
this.information = information; this.information = information;
this.condition = condition; this.condition = condition;
this.dependencies = dependencies; this.dependencies = dependencies;
this.optionalDependencies = optionalDependencies; this.optionalDependencies = optionalDependencies;
this.url = url; this.url = url;
this.checksum = checksum; this.checksum = checksum;
this.installLink = installLink;
} }
public String getUrl() { public String getUrl() {
@@ -85,4 +95,8 @@ public class AvailablePluginDescriptor implements PluginDescriptor {
public Set<String> getOptionalDependencies() { public Set<String> getOptionalDependencies() {
return optionalDependencies; return optionalDependencies;
} }
public Optional<String> getInstallLink() {
return Optional.ofNullable(installLink);
}
} }

View File

@@ -24,8 +24,6 @@
package sonia.scm.plugin; package sonia.scm.plugin;
//~--- non-JDK imports --------------------------------------------------------
import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.PermissionObject;
import com.github.sdorra.ssp.StaticPermissions; import com.github.sdorra.ssp.StaticPermissions;
import lombok.Data; import lombok.Data;
@@ -37,11 +35,6 @@ import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable; import java.io.Serializable;
//~--- JDK imports ------------------------------------------------------------
/**
* @author Sebastian Sdorra
*/
@Data @Data
@StaticPermissions( @StaticPermissions(
value = "plugin", value = "plugin",
@@ -63,6 +56,7 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea
private String author; private String author;
private String category; private String category;
private String avatarUrl; private String avatarUrl;
private PluginType type = PluginType.SCM;
@Override @Override
public PluginInformation clone() { public PluginInformation clone() {
@@ -74,6 +68,7 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea
clone.setAuthor(author); clone.setAuthor(author);
clone.setCategory(category); clone.setCategory(category);
clone.setAvatarUrl(avatarUrl); clone.setAvatarUrl(avatarUrl);
clone.setType(type);
return clone; return clone;
} }
@@ -95,4 +90,9 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea
public boolean isValid() { public boolean isValid() {
return Util.isNotEmpty(name) && Util.isNotEmpty(version); return Util.isNotEmpty(name) && Util.isNotEmpty(version);
} }
public enum PluginType {
SCM,
CLOUDOGU
}
} }

View File

@@ -34,7 +34,7 @@ import {
useInstallPlugin, useInstallPlugin,
usePendingPlugins, usePendingPlugins,
useUninstallPlugin, useUninstallPlugin,
useUpdatePlugins useUpdatePlugins,
} from "./plugins"; } from "./plugins";
import { act } from "react-test-renderer"; import { act } from "react-test-renderer";
@@ -48,12 +48,13 @@ describe("Test plugin hooks", () => {
pending: false, pending: false,
dependencies: [], dependencies: [],
optionalDependencies: [], optionalDependencies: [],
type: "SCM",
_links: { _links: {
install: { href: "/plugins/available/heart-of-gold-plugin/install" }, install: { href: "/plugins/available/heart-of-gold-plugin/install" },
installWithRestart: { installWithRestart: {
href: "/plugins/available/heart-of-gold-plugin/install?restart=true" href: "/plugins/available/heart-of-gold-plugin/install?restart=true",
} },
} },
}; };
const installedPlugin: Plugin = { const installedPlugin: Plugin = {
@@ -66,23 +67,24 @@ describe("Test plugin hooks", () => {
markedForUninstall: false, markedForUninstall: false,
dependencies: [], dependencies: [],
optionalDependencies: [], optionalDependencies: [],
type: "SCM",
_links: { _links: {
self: { self: {
href: "/plugins/installed/heart-of-gold-plugin" href: "/plugins/installed/heart-of-gold-plugin",
}, },
update: { update: {
href: "/plugins/available/heart-of-gold-plugin/install" href: "/plugins/available/heart-of-gold-plugin/install",
}, },
updateWithRestart: { updateWithRestart: {
href: "/plugins/available/heart-of-gold-plugin/install?restart=true" href: "/plugins/available/heart-of-gold-plugin/install?restart=true",
}, },
uninstall: { uninstall: {
href: "/plugins/installed/heart-of-gold-plugin/uninstall" href: "/plugins/installed/heart-of-gold-plugin/uninstall",
}, },
uninstallWithRestart: { uninstallWithRestart: {
href: "/plugins/installed/heart-of-gold-plugin/uninstall?restart=true" href: "/plugins/installed/heart-of-gold-plugin/uninstall?restart=true",
} },
} },
}; };
const installedCorePlugin: Plugin = { const installedCorePlugin: Plugin = {
@@ -93,24 +95,25 @@ describe("Test plugin hooks", () => {
name: "heart-of-gold-core-plugin", name: "heart-of-gold-core-plugin",
pending: false, pending: false,
markedForUninstall: false, markedForUninstall: false,
type: "SCM",
dependencies: [], dependencies: [],
optionalDependencies: [], optionalDependencies: [],
_links: { _links: {
self: { self: {
href: "/plugins/installed/heart-of-gold-core-plugin" href: "/plugins/installed/heart-of-gold-core-plugin",
} },
} },
}; };
const createPluginCollection = (plugins: Plugin[]): PluginCollection => ({ const createPluginCollection = (plugins: Plugin[]): PluginCollection => ({
_links: { _links: {
update: { update: {
href: "/plugins/update" href: "/plugins/update",
} },
}, },
_embedded: { _embedded: {
plugins plugins,
} },
}); });
const createPendingPlugins = ( const createPendingPlugins = (
@@ -122,8 +125,8 @@ describe("Test plugin hooks", () => {
_embedded: { _embedded: {
new: newPlugins, new: newPlugins,
update: updatePlugins, update: updatePlugins,
uninstall: uninstallPlugins uninstall: uninstallPlugins,
} },
}); });
afterEach(() => fetchMock.reset()); afterEach(() => fetchMock.reset());
@@ -135,7 +138,7 @@ describe("Test plugin hooks", () => {
setIndexLink(queryClient, "availablePlugins", "/availablePlugins"); setIndexLink(queryClient, "availablePlugins", "/availablePlugins");
fetchMock.get("/api/v2/availablePlugins", availablePlugins); fetchMock.get("/api/v2/availablePlugins", availablePlugins);
const { result, waitFor } = renderHook(() => useAvailablePlugins(), { const { result, waitFor } = renderHook(() => useAvailablePlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await waitFor(() => !!result.current.data); await waitFor(() => !!result.current.data);
expect(result.current.data).toEqual(availablePlugins); expect(result.current.data).toEqual(availablePlugins);
@@ -149,7 +152,7 @@ describe("Test plugin hooks", () => {
setIndexLink(queryClient, "installedPlugins", "/installedPlugins"); setIndexLink(queryClient, "installedPlugins", "/installedPlugins");
fetchMock.get("/api/v2/installedPlugins", installedPlugins); fetchMock.get("/api/v2/installedPlugins", installedPlugins);
const { result, waitFor } = renderHook(() => useInstalledPlugins(), { const { result, waitFor } = renderHook(() => useInstalledPlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await waitFor(() => !!result.current.data); await waitFor(() => !!result.current.data);
expect(result.current.data).toEqual(installedPlugins); expect(result.current.data).toEqual(installedPlugins);
@@ -163,7 +166,7 @@ describe("Test plugin hooks", () => {
setIndexLink(queryClient, "pendingPlugins", "/pendingPlugins"); setIndexLink(queryClient, "pendingPlugins", "/pendingPlugins");
fetchMock.get("/api/v2/pendingPlugins", pendingPlugins); fetchMock.get("/api/v2/pendingPlugins", pendingPlugins);
const { result, waitFor } = renderHook(() => usePendingPlugins(), { const { result, waitFor } = renderHook(() => usePendingPlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await waitFor(() => !!result.current.data); await waitFor(() => !!result.current.data);
expect(result.current.data).toEqual(pendingPlugins); expect(result.current.data).toEqual(pendingPlugins);
@@ -179,7 +182,7 @@ describe("Test plugin hooks", () => {
fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install?restart=true", installedPlugin); fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install?restart=true", installedPlugin);
fetchMock.get("/api/v2/", "Restarted"); fetchMock.get("/api/v2/", "Restarted");
const { result, waitFor, waitForNextUpdate } = renderHook(() => useInstallPlugin(), { const { result, waitFor, waitForNextUpdate } = renderHook(() => useInstallPlugin(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { install } = result.current; const { install } = result.current;
@@ -199,7 +202,7 @@ describe("Test plugin hooks", () => {
queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); queryClient.setQueryData(["plugins", "pending"], createPendingPlugins());
fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install", installedPlugin); fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install", installedPlugin);
const { result, waitForNextUpdate } = renderHook(() => useInstallPlugin(), { const { result, waitForNextUpdate } = renderHook(() => useInstallPlugin(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { install } = result.current; const { install } = result.current;
@@ -221,7 +224,7 @@ describe("Test plugin hooks", () => {
fetchMock.post("/api/v2/plugins/installed/heart-of-gold-plugin/uninstall?restart=true", availablePlugin); fetchMock.post("/api/v2/plugins/installed/heart-of-gold-plugin/uninstall?restart=true", availablePlugin);
fetchMock.get("/api/v2/", "Restarted"); fetchMock.get("/api/v2/", "Restarted");
const { result, waitForNextUpdate, waitFor } = renderHook(() => useUninstallPlugin(), { const { result, waitForNextUpdate, waitFor } = renderHook(() => useUninstallPlugin(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { uninstall } = result.current; const { uninstall } = result.current;
@@ -241,7 +244,7 @@ describe("Test plugin hooks", () => {
queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); queryClient.setQueryData(["plugins", "pending"], createPendingPlugins());
fetchMock.post("/api/v2/plugins/installed/heart-of-gold-plugin/uninstall", availablePlugin); fetchMock.post("/api/v2/plugins/installed/heart-of-gold-plugin/uninstall", availablePlugin);
const { result, waitForNextUpdate } = renderHook(() => useUninstallPlugin(), { const { result, waitForNextUpdate } = renderHook(() => useUninstallPlugin(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { uninstall } = result.current; const { uninstall } = result.current;
@@ -263,7 +266,7 @@ describe("Test plugin hooks", () => {
fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install?restart=true", installedPlugin); fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install?restart=true", installedPlugin);
fetchMock.get("/api/v2/", "Restarted"); fetchMock.get("/api/v2/", "Restarted");
const { result, waitForNextUpdate, waitFor } = renderHook(() => useUpdatePlugins(), { const { result, waitForNextUpdate, waitFor } = renderHook(() => useUpdatePlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { update } = result.current; const { update } = result.current;
@@ -282,7 +285,7 @@ describe("Test plugin hooks", () => {
queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); queryClient.setQueryData(["plugins", "pending"], createPendingPlugins());
fetchMock.post("/api/v2/plugins/update", installedPlugin); fetchMock.post("/api/v2/plugins/update", installedPlugin);
const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), { const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { update } = result.current; const { update } = result.current;
@@ -300,7 +303,7 @@ describe("Test plugin hooks", () => {
queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); queryClient.setQueryData(["plugins", "pending"], createPendingPlugins());
fetchMock.post("/api/v2/plugins/update", installedPlugin); fetchMock.post("/api/v2/plugins/update", installedPlugin);
const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), { const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { update } = result.current; const { update } = result.current;
@@ -318,7 +321,7 @@ describe("Test plugin hooks", () => {
queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); queryClient.setQueryData(["plugins", "pending"], createPendingPlugins());
fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install", installedPlugin); fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install", installedPlugin);
const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), { const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), {
wrapper: createWrapper(undefined, queryClient) wrapper: createWrapper(undefined, queryClient),
}); });
await act(() => { await act(() => {
const { update } = result.current; const { update } = result.current;

View File

@@ -24,6 +24,8 @@
import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal"; import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal";
type PluginType = "SCM" | "CLOUDOGU";
export type Plugin = HalRepresentation & { export type Plugin = HalRepresentation & {
name: string; name: string;
version: string; version: string;
@@ -34,6 +36,7 @@ export type Plugin = HalRepresentation & {
category: string; category: string;
avatarUrl?: string; avatarUrl?: string;
pending: boolean; pending: boolean;
type: PluginType;
markedForUninstall?: boolean; markedForUninstall?: boolean;
dependencies: string[]; dependencies: string[];
optionalDependencies: string[]; optionalDependencies: string[];
@@ -43,7 +46,8 @@ export type PluginCollection = HalRepresentationWithEmbedded<{
plugins: Plugin[]; plugins: Plugin[];
}>; }>;
export const isPluginCollection = (input: HalRepresentation): input is PluginCollection => input._embedded ? "plugins" in input._embedded : false; export const isPluginCollection = (input: HalRepresentation): input is PluginCollection =>
input._embedded ? "plugins" in input._embedded : false;
export type PluginGroup = { export type PluginGroup = {
name: string; name: string;

View File

@@ -47,7 +47,8 @@
"title": { "title": {
"install": "{{name}} Plugin installieren", "install": "{{name}} Plugin installieren",
"update": "{{name}} Plugin aktualisieren", "update": "{{name}} Plugin aktualisieren",
"uninstall": "{{name}} Plugin deinstallieren" "uninstall": "{{name}} Plugin deinstallieren",
"cloudoguInstall": "Plugin über myCloudogu installieren"
}, },
"restart": "Neustarten, um Plugin-Änderungen wirksam zu machen", "restart": "Neustarten, um Plugin-Änderungen wirksam zu machen",
"install": "Installieren", "install": "Installieren",
@@ -67,6 +68,8 @@
"version": "Version", "version": "Version",
"currentVersion": "Installierte Version", "currentVersion": "Installierte Version",
"newVersion": "Neue Version", "newVersion": "Neue Version",
"cloudoguInstallInfo": "Dieses Plugin ist nur über myCloudogu erhältlich. Zum Installieren folgen Sie bitte der Anleitung.",
"cloudoguInstall": "Zur Installationsanleitung",
"dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert bzw. aktualisiert, wenn sie noch nicht in der aktuellen Version vorhanden sind!", "dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert bzw. aktualisiert, wenn sie noch nicht in der aktuellen Version vorhanden sind!",
"optionalDependencyNotification": "Mit diesem Plugin werden folgende optionale Abhängigkeiten mit aktualisiert, falls sie installiert sind!", "optionalDependencyNotification": "Mit diesem Plugin werden folgende optionale Abhängigkeiten mit aktualisiert, falls sie installiert sind!",
"dependencies": "Abhängigkeiten", "dependencies": "Abhängigkeiten",

View File

@@ -47,7 +47,8 @@
"title": { "title": {
"install": "Install {{name}} Plugin", "install": "Install {{name}} Plugin",
"update": "Update {{name}} Plugin", "update": "Update {{name}} Plugin",
"uninstall": "Uninstall {{name}} Plugin" "uninstall": "Uninstall {{name}} Plugin",
"cloudoguInstall": "Get plugin from myCloudogu"
}, },
"restart": "Restart to make plugin changes effective", "restart": "Restart to make plugin changes effective",
"install": "Install", "install": "Install",
@@ -67,6 +68,8 @@
"version": "Version", "version": "Version",
"currentVersion": "Installed version", "currentVersion": "Installed version",
"newVersion": "New version", "newVersion": "New version",
"cloudoguInstallInfo": "This plugin is only available via myCloudogu. Follow the instructions to install it.",
"cloudoguInstall": "To Installation Instructions",
"dependencyNotification": "With this plugin, the following dependencies will be installed/updated if their latest versions are not installed yet!", "dependencyNotification": "With this plugin, the following dependencies will be installed/updated if their latest versions are not installed yet!",
"optionalDependencyNotification": "With this plugin, the following optional dependencies will be updated if they are installed!", "optionalDependencyNotification": "With this plugin, the following optional dependencies will be updated if they are installed!",
"dependencies": "Dependencies", "dependencies": "Dependencies",

View File

@@ -58,12 +58,18 @@ const PluginEntry: FC<Props> = ({ plugin, openModal }) => {
const isInstallable = plugin._links.install && (plugin._links.install as Link).href; const isInstallable = plugin._links.install && (plugin._links.install as Link).href;
const isUpdatable = plugin._links.update && (plugin._links.update as Link).href; const isUpdatable = plugin._links.update && (plugin._links.update as Link).href;
const isUninstallable = plugin._links.uninstall && (plugin._links.uninstall as Link).href; const isUninstallable = plugin._links.uninstall && (plugin._links.uninstall as Link).href;
const isCloudoguPlugin = plugin.type === "CLOUDOGU";
const pendingSpinner = () => ( const pendingSpinner = () => (
<Icon className="fa-spin fa-lg" name="spinner" color={plugin.markedForUninstall ? "danger" : "info"} /> <Icon className="fa-spin fa-lg" name="spinner" color={plugin.markedForUninstall ? "danger" : "info"} />
); );
const actionBar = () => ( const actionBar = () => (
<ActionbarWrapper className="is-flex"> <ActionbarWrapper className="is-flex">
{isCloudoguPlugin && (
<IconWrapper className="level-item" onClick={() => openModal({ plugin, action: PluginAction.CLOUDOGU })}>
<Icon title={t("plugins.modal.cloudoguInstall")} name="link" color="success-dark" />
</IconWrapper>
)}
{isInstallable && ( {isInstallable && (
<IconWrapper className="level-item" onClick={() => openModal({ plugin, action: PluginAction.INSTALL })}> <IconWrapper className="level-item" onClick={() => openModal({ plugin, action: PluginAction.INSTALL })}>
<Icon title={t("plugins.modal.install")} name="download" color="info" /> <Icon title={t("plugins.modal.install")} name="download" color="info" />

View File

@@ -33,7 +33,7 @@ type Props = {
}; };
const PluginGroupEntry: FC<Props> = ({ openModal, group }) => { const PluginGroupEntry: FC<Props> = ({ openModal, group }) => {
const entries = group.plugins.map(plugin => { const entries = group.plugins.map((plugin) => {
return <PluginEntry plugin={plugin} openModal={openModal} key={plugin.name} />; return <PluginEntry plugin={plugin} openModal={openModal} key={plugin.name} />;
}); });
return <CardColumnGroup name={group.name} elements={entries} />; return <CardColumnGroup name={group.name} elements={entries} />;

View File

@@ -36,7 +36,7 @@ const PluginList: FC<Props> = ({ plugins, openModal }) => {
const groups = groupByCategory(plugins); const groups = groupByCategory(plugins);
return ( return (
<div className="content is-plugin-page"> <div className="content is-plugin-page">
{groups.map(group => { {groups.map((group) => {
return <PluginGroupEntry group={group} openModal={openModal} key={group.name} />; return <PluginGroupEntry group={group} openModal={openModal} key={group.name} />;
})} })}
</div> </div>

View File

@@ -25,7 +25,7 @@ import React, { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import classNames from "classnames"; import classNames from "classnames";
import styled from "styled-components"; import styled from "styled-components";
import { Plugin } from "@scm-manager/ui-types"; import { Link, Plugin } from "@scm-manager/ui-types";
import { Button, ButtonGroup, Checkbox, ErrorNotification, Modal, Notification } from "@scm-manager/ui-components"; import { Button, ButtonGroup, Checkbox, ErrorNotification, Modal, Notification } from "@scm-manager/ui-components";
import SuccessNotification from "./SuccessNotification"; import SuccessNotification from "./SuccessNotification";
import { useInstallPlugin, useUninstallPlugin, useUpdatePlugins } from "@scm-manager/ui-api"; import { useInstallPlugin, useUninstallPlugin, useUpdatePlugins } from "@scm-manager/ui-api";
@@ -33,13 +33,17 @@ import { PluginAction } from "../containers/PluginsOverview";
type Props = { type Props = {
plugin: Plugin; plugin: Plugin;
pluginAction: string; pluginAction: PluginAction;
onClose: () => void; onClose: () => void;
}; };
const ListParent = styled.div` type ParentWithPluginAction = {
pluginAction?: PluginAction;
};
const ListParent = styled.div.attrs((props) => ({}))<ParentWithPluginAction>`
margin-right: 0; margin-right: 0;
min-width: ${props => (props.pluginAction === PluginAction.INSTALL ? "5.5em" : "10em")}; min-width: ${(props) => (props.pluginAction === PluginAction.INSTALL ? "5.5em" : "10em")};
text-align: left; text-align: left;
`; `;
@@ -63,9 +67,12 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
} }
}, [isDone]); }, [isDone]);
const handlePluginAction = (e: Event) => { const handlePluginAction = (e: React.MouseEvent<Element, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
switch (pluginAction) { switch (pluginAction) {
case PluginAction.CLOUDOGU:
window.open((plugin._links.cloudoguInstall as Link).href, "_blank");
break;
case PluginAction.INSTALL: case PluginAction.INSTALL:
install(plugin, { restart: shouldRestart }); install(plugin, { restart: shouldRestart });
break; break;
@@ -76,7 +83,7 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
update(plugin, { restart: shouldRestart }); update(plugin, { restart: shouldRestart });
break; break;
default: default:
throw new Error(`Unkown plugin action ${pluginAction}`); throw new Error(`Unknown plugin action ${pluginAction}`);
} }
}; };
@@ -172,7 +179,7 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
disabled={false} disabled={false}
/> />
); );
} else { } else if (pluginAction !== PluginAction.CLOUDOGU) {
return <Notification type="warning">{t("plugins.modal.manualRestartRequired")}</Notification>; return <Notification type="warning">{t("plugins.modal.manualRestartRequired")}</Notification>;
} }
}; };
@@ -192,6 +199,13 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
</ListParent> </ListParent>
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.author}</ListChild> <ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.author}</ListChild>
</div> </div>
{pluginAction === PluginAction.CLOUDOGU && (
<div className="field is-horizontal">
<Notification type="info" className="is-full-width">
{t("plugins.modal.cloudoguInstallInfo")}
</Notification>
</div>
)}
{pluginAction === PluginAction.INSTALL && ( {pluginAction === PluginAction.INSTALL && (
<div className="field is-horizontal"> <div className="field is-horizontal">
<ListParent className={classNames("field-label", "is-inline-flex")} pluginAction={pluginAction}> <ListParent className={classNames("field-label", "is-inline-flex")} pluginAction={pluginAction}>
@@ -230,7 +244,7 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
return ( return (
<Modal <Modal
title={t(`plugins.modal.title.${pluginAction}`, { title={t(`plugins.modal.title.${pluginAction}`, {
name: plugin.displayName ? plugin.displayName : plugin.name name: plugin.displayName ? plugin.displayName : plugin.name,
})} })}
closeFunction={onClose} closeFunction={onClose}
body={body} body={body}

View File

@@ -32,7 +32,7 @@ import {
Loading, Loading,
Notification, Notification,
Subtitle, Subtitle,
Title Title,
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import PluginsList from "../components/PluginList"; import PluginsList from "../components/PluginList";
import PluginTopActions from "../components/PluginTopActions"; import PluginTopActions from "../components/PluginTopActions";
@@ -47,13 +47,14 @@ import PluginModal from "../components/PluginModal";
export enum PluginAction { export enum PluginAction {
INSTALL = "install", INSTALL = "install",
UPDATE = "update", UPDATE = "update",
UNINSTALL = "uninstall" UNINSTALL = "uninstall",
CLOUDOGU = "cloudoguInstall",
} }
export type PluginModalContent = { export type PluginModalContent = {
plugin: Plugin; plugin: Plugin;
action: PluginAction; action: PluginAction;
} };
type Props = { type Props = {
installed: boolean; installed: boolean;
@@ -64,12 +65,12 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
const { const {
data: availablePlugins, data: availablePlugins,
isLoading: isLoadingAvailablePlugins, isLoading: isLoadingAvailablePlugins,
error: availablePluginsError error: availablePluginsError,
} = useAvailablePlugins({ enabled: !installed }); } = useAvailablePlugins({ enabled: !installed });
const { const {
data: installedPlugins, data: installedPlugins,
isLoading: isLoadingInstalledPlugins, isLoading: isLoadingInstalledPlugins,
error: installedPluginsError error: installedPluginsError,
} = useInstalledPlugins({ enabled: installed }); } = useInstalledPlugins({ enabled: installed });
const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins(); const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins();
const [showPendingModal, setShowPendingModal] = useState(false); const [showPendingModal, setShowPendingModal] = useState(false);
@@ -166,7 +167,7 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
const computeUpdateAllSize = () => { const computeUpdateAllSize = () => {
const outdatedPlugins = collection?._embedded.plugins.filter((p: Plugin) => p._links.update).length; const outdatedPlugins = collection?._embedded.plugins.filter((p: Plugin) => p._links.update).length;
return t("plugins.outdatedPlugins", { return t("plugins.outdatedPlugins", {
count: outdatedPlugins count: outdatedPlugins,
}); });
}; };
@@ -187,28 +188,14 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
); );
} }
if (showCancelModal && pendingPlugins) { if (showCancelModal && pendingPlugins) {
return ( return <CancelPendingActionModal onClose={() => setShowCancelModal(false)} pendingPlugins={pendingPlugins} />;
<CancelPendingActionModal
onClose={() => setShowCancelModal(false)}
pendingPlugins={pendingPlugins}
/>
);
} }
if (showUpdateAllModal && collection) { if (showUpdateAllModal && collection) {
return ( return <UpdateAllActionModal onClose={() => setShowUpdateAllModal(false)} installedPlugins={collection} />;
<UpdateAllActionModal
onClose={() => setShowUpdateAllModal(false)}
installedPlugins={collection}
/>
);
} }
if (pluginModalContent) { if (pluginModalContent) {
const { action, plugin } = pluginModalContent; const { action, plugin } = pluginModalContent;
return <PluginModal return <PluginModal plugin={plugin} pluginAction={action} onClose={() => setPluginModalContent(null)} />;
plugin={plugin}
pluginAction={action}
onClose={() => setPluginModalContent(null)}
/>
} }
return null; return null;
}; };

View File

@@ -24,7 +24,6 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -98,6 +97,7 @@ public class AvailablePluginResource {
PluginPermissions.read().check(); PluginPermissions.read().check();
List<InstalledPlugin> installed = pluginManager.getInstalled(); List<InstalledPlugin> installed = pluginManager.getInstalled();
List<AvailablePlugin> available = pluginManager.getAvailable().stream().filter(a -> notInstalled(a, installed)).collect(Collectors.toList()); List<AvailablePlugin> available = pluginManager.getAvailable().stream().filter(a -> notInstalled(a, installed)).collect(Collectors.toList());
return Response.ok(collectionMapper.mapAvailable(available)).build(); return Response.ok(collectionMapper.mapAvailable(available)).build();
} }

View File

@@ -30,6 +30,7 @@ import de.otto.edison.hal.Links;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import sonia.scm.plugin.PluginInformation;
import java.util.Set; import java.util.Set;
@@ -48,6 +49,7 @@ public class PluginDto extends HalRepresentation {
private String author; private String author;
private String category; private String category;
private String avatarUrl; private String avatarUrl;
private PluginInformation.PluginType type = PluginInformation.PluginType.SCM;
private boolean pending; private boolean pending;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean core; private Boolean core;

View File

@@ -36,7 +36,6 @@ import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginPermissions;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -71,6 +70,9 @@ public abstract class PluginDtoMapper {
PluginDto dto = createDtoForAvailable(plugin); PluginDto dto = createDtoForAvailable(plugin);
map(dto, plugin); map(dto, plugin);
dto.setPending(plugin.isPending()); dto.setPending(plugin.isPending());
if (dto.getType() == null) {
dto.setType(PluginInformation.PluginType.SCM);
}
return dto; return dto;
} }
@@ -91,8 +93,14 @@ public abstract class PluginDtoMapper {
.self(information.getName())); .self(information.getName()));
if (!plugin.isPending() && PluginPermissions.write().isPermitted()) { if (!plugin.isPending() && PluginPermissions.write().isPermitted()) {
String href = resourceLinks.availablePlugin().install(information.getName()); boolean isCloudoguPlugin = plugin.getDescriptor().getInformation().getType() == PluginInformation.PluginType.CLOUDOGU;
appendLink(links, "install", href); if (isCloudoguPlugin) {
Optional<String> cloudoguInstallLink = plugin.getDescriptor().getInstallLink();
cloudoguInstallLink.ifPresent(link -> links.single(link("cloudoguInstall", link)));
} else {
String href = resourceLinks.availablePlugin().install(information.getName());
appendLink(links, "install", href);
}
} }
return new PluginDto(links.build()); return new PluginDto(links.build());

View File

@@ -37,9 +37,9 @@ import javax.ws.rs.Path;
@Path("v2/plugins") @Path("v2/plugins")
public class PluginRootResource { public class PluginRootResource {
private Provider<InstalledPluginResource> installedPluginResourceProvider; private final Provider<InstalledPluginResource> installedPluginResourceProvider;
private Provider<AvailablePluginResource> availablePluginResourceProvider; private final Provider<AvailablePluginResource> availablePluginResourceProvider;
private Provider<PendingPluginResource> pendingPluginResourceProvider; private final Provider<PendingPluginResource> pendingPluginResourceProvider;
@Inject @Inject
public PluginRootResource(Provider<InstalledPluginResource> installedPluginResourceProvider, Provider<AvailablePluginResource> availablePluginResourceProvider, Provider<PendingPluginResource> pendingPluginResourceProvider) { public PluginRootResource(Provider<InstalledPluginResource> installedPluginResourceProvider, Provider<AvailablePluginResource> availablePluginResourceProvider, Provider<PendingPluginResource> pendingPluginResourceProvider) {

View File

@@ -70,26 +70,27 @@ public final class PluginCenterDto implements Serializable {
@AllArgsConstructor @AllArgsConstructor
public static class Plugin { public static class Plugin {
private String name; private final String name;
private String version; private final String version;
private String displayName; private final String displayName;
private String description; private final String description;
private String category; private final String category;
private String author; private final String author;
private String avatarUrl; private final String avatarUrl;
private String sha256sum; private final String sha256sum;
private PluginInformation.PluginType type;
@XmlElement(name = "conditions") @XmlElement(name = "conditions")
private Condition conditions; private final Condition conditions;
@XmlElement(name = "dependencies") @XmlElement(name = "dependencies")
private Set<String> dependencies; private final Set<String> dependencies;
@XmlElement(name = "optionalDependencies") @XmlElement(name = "optionalDependencies")
private Set<String> optionalDependencies; private final Set<String> optionalDependencies;
@XmlElement(name = "_links") @XmlElement(name = "_links")
private Map<String, Link> links; private final Map<String, Link> links;
} }
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@@ -98,9 +99,9 @@ public final class PluginCenterDto implements Serializable {
@AllArgsConstructor @AllArgsConstructor
public static class Condition { public static class Condition {
private List<String> os; private final List<String> os;
private String arch; private final String arch;
private String minVersion; private final String minVersion;
} }
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)

View File

@@ -31,22 +31,32 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
@Mapper @Mapper
public abstract class PluginCenterDtoMapper { public abstract class PluginCenterDtoMapper {
static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class); static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class);
abstract PluginInformation map(PluginCenterDto.Plugin plugin); abstract PluginInformation map(PluginCenterDto.Plugin plugin);
abstract PluginCondition map(PluginCenterDto.Condition condition); abstract PluginCondition map(PluginCenterDto.Condition condition);
Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) { Set<AvailablePlugin> map(PluginCenterDto pluginCenterDto) {
Set<AvailablePlugin> plugins = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) { for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) {
String url = plugin.getLinks().get("download").getHref(); String url = plugin.getLinks().get("download").getHref();
String installLink = getInstallLink(plugin);
AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor(
map(plugin), map(plugin.getConditions()), plugin.getDependencies(), plugin.getOptionalDependencies(), url, plugin.getSha256sum() map(plugin), map(plugin.getConditions()), plugin.getDependencies(), plugin.getOptionalDependencies(), url, plugin.getSha256sum(), installLink
); );
plugins.add(new AvailablePlugin(descriptor)); plugins.add(new AvailablePlugin(descriptor));
} }
return plugins; return plugins;
} }
private String getInstallLink(PluginCenterDto.Plugin plugin) {
PluginCenterDto.Link link = plugin.getLinks().get("install");
if (link != null) {
return link.getHref();
}
return null;
}
} }

View File

@@ -44,6 +44,7 @@ import java.net.URI;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sonia.scm.plugin.PluginInformation.PluginType.*;
import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createAvailable;
import static sonia.scm.plugin.PluginTestHelper.createInstalled; import static sonia.scm.plugin.PluginTestHelper.createInstalled;
@@ -85,9 +86,14 @@ class PluginDtoMapperTest {
assertThat(dto.getAuthor()).isEqualTo("Sebastian Sdorra"); assertThat(dto.getAuthor()).isEqualTo("Sebastian Sdorra");
assertThat(dto.getCategory()).isEqualTo("Authentication"); assertThat(dto.getCategory()).isEqualTo("Authentication");
assertThat(dto.getAvatarUrl()).isEqualTo("https://avatar.scm-manager.org/plugins/cas.png"); assertThat(dto.getAvatarUrl()).isEqualTo("https://avatar.scm-manager.org/plugins/cas.png");
assertThat(dto.getType()).isEqualTo(SCM);
} }
private PluginInformation createPluginInformation() { private PluginInformation createPluginInformation() {
return createPluginInformation(SCM);
}
private PluginInformation createPluginInformation(PluginInformation.PluginType type) {
PluginInformation information = new PluginInformation(); PluginInformation information = new PluginInformation();
information.setName("scm-cas-plugin"); information.setName("scm-cas-plugin");
information.setVersion("1.0.0"); information.setVersion("1.0.0");
@@ -95,6 +101,7 @@ class PluginDtoMapperTest {
information.setAuthor("Sebastian Sdorra"); information.setAuthor("Sebastian Sdorra");
information.setCategory("Authentication"); information.setCategory("Authentication");
information.setAvatarUrl("https://avatar.scm-manager.org/plugins/cas.png"); information.setAvatarUrl("https://avatar.scm-manager.org/plugins/cas.png");
information.setType(type);
return information; return information;
} }
@@ -135,6 +142,18 @@ class PluginDtoMapperTest {
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install"); .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install");
} }
@Test
void shouldAppendCloudoguInstallLink() {
when(subject.isPermitted("plugin:write")).thenReturn(true);
AvailablePlugin plugin = createAvailable(createPluginInformation(CLOUDOGU));
PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getType()).isEqualTo(CLOUDOGU);
assertThat(dto.getLinks().getLinkBy("cloudoguInstall").get().getHref())
.isEqualTo("mycloudogu.com/install/my_plugin");
}
@Test @Test
void shouldAppendInstallWithRestartLink() { void shouldAppendInstallWithRestartLink() {
when(restarter.isSupported()).thenReturn(true); when(restarter.isSupported()).thenReturn(true);

View File

@@ -58,13 +58,14 @@ class PluginCenterDtoMapperTest {
void shouldMapSinglePlugin() { void shouldMapSinglePlugin() {
Plugin plugin = new Plugin( Plugin plugin = new Plugin(
"scm-hitchhiker-plugin", "scm-hitchhiker-plugin",
"2.0.0",
"SCM Hitchhiker Plugin", "SCM Hitchhiker Plugin",
"plugin for hitchhikers", "plugin for hitchhikers",
"Travel", "Travel",
"2.0.0",
"trillian", "trillian",
"http://avatar.url", "http://avatar.url",
"555000444", "555000444",
PluginInformation.PluginType.SCM,
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
ImmutableSet.of("scm-review-plugin"), ImmutableSet.of("scm-review-plugin"),
ImmutableSet.of(), ImmutableSet.of(),
@@ -93,13 +94,14 @@ class PluginCenterDtoMapperTest {
void shouldMapMultiplePlugins() { void shouldMapMultiplePlugins() {
Plugin plugin1 = new Plugin( Plugin plugin1 = new Plugin(
"scm-review-plugin", "scm-review-plugin",
"2.1.0",
"SCM Hitchhiker Plugin", "SCM Hitchhiker Plugin",
"plugin for hitchhikers", "plugin for hitchhikers",
"Travel", "Travel",
"2.1.0",
"trillian", "trillian",
"https://avatar.url", "https://avatar.url",
"12345678aa", "12345678aa",
PluginInformation.PluginType.SCM,
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
ImmutableSet.of("scm-review-plugin"), ImmutableSet.of("scm-review-plugin"),
ImmutableSet.of(), ImmutableSet.of(),
@@ -108,13 +110,14 @@ class PluginCenterDtoMapperTest {
Plugin plugin2 = new Plugin( Plugin plugin2 = new Plugin(
"scm-hitchhiker-plugin", "scm-hitchhiker-plugin",
"2.0.0",
"SCM Hitchhiker Plugin", "SCM Hitchhiker Plugin",
"plugin for hitchhikers", "plugin for hitchhikers",
"Travel", "Travel",
"2.0.0",
"dent", "dent",
"http://avatar.url", "http://avatar.url",
"555000444", "555000444",
PluginInformation.PluginType.CLOUDOGU,
new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), new Condition(Collections.singletonList("linux"), "amd64","2.0.0"),
ImmutableSet.of("scm-review-plugin"), ImmutableSet.of("scm-review-plugin"),
ImmutableSet.of(), ImmutableSet.of(),
@@ -132,6 +135,8 @@ class PluginCenterDtoMapperTest {
assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion()); assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion());
assertThat(pluginInformation2.getAuthor()).isEqualTo(plugin2.getAuthor()); assertThat(pluginInformation2.getAuthor()).isEqualTo(plugin2.getAuthor());
assertThat(pluginInformation2.getVersion()).isEqualTo(plugin2.getVersion()); assertThat(pluginInformation2.getVersion()).isEqualTo(plugin2.getVersion());
assertThat(pluginInformation1.getType()).isEqualTo(PluginInformation.PluginType.SCM);
assertThat(pluginInformation2.getType()).isEqualTo(PluginInformation.PluginType.CLOUDOGU);
assertThat(resultSet.size()).isEqualTo(2); assertThat(resultSet.size()).isEqualTo(2);
} }

View File

@@ -26,6 +26,8 @@ package sonia.scm.plugin;
import org.mockito.Answers; import org.mockito.Answers;
import java.util.Optional;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -62,6 +64,7 @@ public class PluginTestHelper {
public static AvailablePlugin createAvailable(PluginInformation information) { public static AvailablePlugin createAvailable(PluginInformation information) {
AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class);
lenient().when(descriptor.getInformation()).thenReturn(information); lenient().when(descriptor.getInformation()).thenReturn(information);
lenient().when(descriptor.getInstallLink()).thenReturn(Optional.of("mycloudogu.com/install/my_plugin"));
return new AvailablePlugin(descriptor); return new AvailablePlugin(descriptor);
} }