From 7a3db7ee3fde37b4abc6e516f59df3f25b3278c4 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Fri, 25 Jun 2021 09:22:53 +0200 Subject: [PATCH] Include cloudogu plugins to plugin center (#1709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: René Pfeuffer --- gradle/changelog/cloudogu_plugins.yaml | 2 + .../scm/plugin/AvailablePluginDescriptor.java | 16 ++++- .../sonia/scm/plugin/PluginInformation.java | 16 ++--- scm-ui/ui-api/src/plugins.test.ts | 65 ++++++++++--------- scm-ui/ui-types/src/Plugin.ts | 6 +- scm-ui/ui-webapp/public/locales/de/admin.json | 5 +- scm-ui/ui-webapp/public/locales/en/admin.json | 5 +- .../admin/plugins/components/PluginEntry.tsx | 6 ++ .../plugins/components/PluginGroupEntry.tsx | 2 +- .../admin/plugins/components/PluginList.tsx | 2 +- .../admin/plugins/components/PluginModal.tsx | 30 ++++++--- .../plugins/containers/PluginsOverview.tsx | 33 +++------- .../v2/resources/AvailablePluginResource.java | 2 +- .../sonia/scm/api/v2/resources/PluginDto.java | 2 + .../scm/api/v2/resources/PluginDtoMapper.java | 14 +++- .../api/v2/resources/PluginRootResource.java | 6 +- .../sonia/scm/plugin/PluginCenterDto.java | 31 ++++----- .../scm/plugin/PluginCenterDtoMapper.java | 16 ++++- .../api/v2/resources/PluginDtoMapperTest.java | 19 ++++++ .../scm/plugin/PluginCenterDtoMapperTest.java | 11 +++- .../sonia/scm/plugin/PluginTestHelper.java | 3 + 21 files changed, 188 insertions(+), 104 deletions(-) create mode 100644 gradle/changelog/cloudogu_plugins.yaml diff --git a/gradle/changelog/cloudogu_plugins.yaml b/gradle/changelog/cloudogu_plugins.yaml new file mode 100644 index 0000000000..f56906eba5 --- /dev/null +++ b/gradle/changelog/cloudogu_plugins.yaml @@ -0,0 +1,2 @@ +- type: Added + description: Prepare plugin center to show cloudogu plugins ([#1709](https://github.com/scm-manager/scm-manager/pull/1709)) diff --git a/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java index e251753113..84746b7dc0 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/plugin/AvailablePluginDescriptor.java @@ -40,22 +40,32 @@ public class AvailablePluginDescriptor implements PluginDescriptor { private final Set optionalDependencies; private final String url; 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 public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set dependencies, String url, String 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 dependencies, Set optionalDependencies, String url, String checksum) { + this(information, condition, dependencies, optionalDependencies, url, checksum, null); + } + + public AvailablePluginDescriptor(PluginInformation information, PluginCondition condition, Set dependencies, Set optionalDependencies, String url, String checksum, String installLink) { this.information = information; this.condition = condition; this.dependencies = dependencies; this.optionalDependencies = optionalDependencies; this.url = url; this.checksum = checksum; + this.installLink = installLink; } public String getUrl() { @@ -85,4 +95,8 @@ public class AvailablePluginDescriptor implements PluginDescriptor { public Set getOptionalDependencies() { return optionalDependencies; } + + public Optional getInstallLink() { + return Optional.ofNullable(installLink); + } } diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java index 20f3751070..7b90cf4d36 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginInformation.java @@ -21,10 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.plugin; import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; @@ -37,11 +35,6 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; -//~--- JDK imports ------------------------------------------------------------ - -/** - * @author Sebastian Sdorra - */ @Data @StaticPermissions( value = "plugin", @@ -63,6 +56,7 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea private String author; private String category; private String avatarUrl; + private PluginType type = PluginType.SCM; @Override public PluginInformation clone() { @@ -74,6 +68,7 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea clone.setAuthor(author); clone.setCategory(category); clone.setAvatarUrl(avatarUrl); + clone.setType(type); return clone; } @@ -95,4 +90,9 @@ public class PluginInformation implements PermissionObject, Validateable, Clonea public boolean isValid() { return Util.isNotEmpty(name) && Util.isNotEmpty(version); } + + public enum PluginType { + SCM, + CLOUDOGU + } } diff --git a/scm-ui/ui-api/src/plugins.test.ts b/scm-ui/ui-api/src/plugins.test.ts index 8be8403d3e..eb8acad819 100644 --- a/scm-ui/ui-api/src/plugins.test.ts +++ b/scm-ui/ui-api/src/plugins.test.ts @@ -34,7 +34,7 @@ import { useInstallPlugin, usePendingPlugins, useUninstallPlugin, - useUpdatePlugins + useUpdatePlugins, } from "./plugins"; import { act } from "react-test-renderer"; @@ -48,12 +48,13 @@ describe("Test plugin hooks", () => { pending: false, dependencies: [], optionalDependencies: [], + type: "SCM", _links: { install: { href: "/plugins/available/heart-of-gold-plugin/install" }, installWithRestart: { - href: "/plugins/available/heart-of-gold-plugin/install?restart=true" - } - } + href: "/plugins/available/heart-of-gold-plugin/install?restart=true", + }, + }, }; const installedPlugin: Plugin = { @@ -66,23 +67,24 @@ describe("Test plugin hooks", () => { markedForUninstall: false, dependencies: [], optionalDependencies: [], + type: "SCM", _links: { self: { - href: "/plugins/installed/heart-of-gold-plugin" + href: "/plugins/installed/heart-of-gold-plugin", }, update: { - href: "/plugins/available/heart-of-gold-plugin/install" + href: "/plugins/available/heart-of-gold-plugin/install", }, updateWithRestart: { - href: "/plugins/available/heart-of-gold-plugin/install?restart=true" + href: "/plugins/available/heart-of-gold-plugin/install?restart=true", }, uninstall: { - href: "/plugins/installed/heart-of-gold-plugin/uninstall" + href: "/plugins/installed/heart-of-gold-plugin/uninstall", }, uninstallWithRestart: { - href: "/plugins/installed/heart-of-gold-plugin/uninstall?restart=true" - } - } + href: "/plugins/installed/heart-of-gold-plugin/uninstall?restart=true", + }, + }, }; const installedCorePlugin: Plugin = { @@ -93,24 +95,25 @@ describe("Test plugin hooks", () => { name: "heart-of-gold-core-plugin", pending: false, markedForUninstall: false, + type: "SCM", dependencies: [], optionalDependencies: [], _links: { self: { - href: "/plugins/installed/heart-of-gold-core-plugin" - } - } + href: "/plugins/installed/heart-of-gold-core-plugin", + }, + }, }; const createPluginCollection = (plugins: Plugin[]): PluginCollection => ({ _links: { update: { - href: "/plugins/update" - } + href: "/plugins/update", + }, }, _embedded: { - plugins - } + plugins, + }, }); const createPendingPlugins = ( @@ -122,8 +125,8 @@ describe("Test plugin hooks", () => { _embedded: { new: newPlugins, update: updatePlugins, - uninstall: uninstallPlugins - } + uninstall: uninstallPlugins, + }, }); afterEach(() => fetchMock.reset()); @@ -135,7 +138,7 @@ describe("Test plugin hooks", () => { setIndexLink(queryClient, "availablePlugins", "/availablePlugins"); fetchMock.get("/api/v2/availablePlugins", availablePlugins); const { result, waitFor } = renderHook(() => useAvailablePlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await waitFor(() => !!result.current.data); expect(result.current.data).toEqual(availablePlugins); @@ -149,7 +152,7 @@ describe("Test plugin hooks", () => { setIndexLink(queryClient, "installedPlugins", "/installedPlugins"); fetchMock.get("/api/v2/installedPlugins", installedPlugins); const { result, waitFor } = renderHook(() => useInstalledPlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await waitFor(() => !!result.current.data); expect(result.current.data).toEqual(installedPlugins); @@ -163,7 +166,7 @@ describe("Test plugin hooks", () => { setIndexLink(queryClient, "pendingPlugins", "/pendingPlugins"); fetchMock.get("/api/v2/pendingPlugins", pendingPlugins); const { result, waitFor } = renderHook(() => usePendingPlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await waitFor(() => !!result.current.data); 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.get("/api/v2/", "Restarted"); const { result, waitFor, waitForNextUpdate } = renderHook(() => useInstallPlugin(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { const { install } = result.current; @@ -199,7 +202,7 @@ describe("Test plugin hooks", () => { queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install", installedPlugin); const { result, waitForNextUpdate } = renderHook(() => useInstallPlugin(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { 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.get("/api/v2/", "Restarted"); const { result, waitForNextUpdate, waitFor } = renderHook(() => useUninstallPlugin(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { const { uninstall } = result.current; @@ -241,7 +244,7 @@ describe("Test plugin hooks", () => { queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); fetchMock.post("/api/v2/plugins/installed/heart-of-gold-plugin/uninstall", availablePlugin); const { result, waitForNextUpdate } = renderHook(() => useUninstallPlugin(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { 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.get("/api/v2/", "Restarted"); const { result, waitForNextUpdate, waitFor } = renderHook(() => useUpdatePlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { const { update } = result.current; @@ -282,7 +285,7 @@ describe("Test plugin hooks", () => { queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); fetchMock.post("/api/v2/plugins/update", installedPlugin); const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { const { update } = result.current; @@ -300,7 +303,7 @@ describe("Test plugin hooks", () => { queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); fetchMock.post("/api/v2/plugins/update", installedPlugin); const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { const { update } = result.current; @@ -318,7 +321,7 @@ describe("Test plugin hooks", () => { queryClient.setQueryData(["plugins", "pending"], createPendingPlugins()); fetchMock.post("/api/v2/plugins/available/heart-of-gold-plugin/install", installedPlugin); const { result, waitForNextUpdate } = renderHook(() => useUpdatePlugins(), { - wrapper: createWrapper(undefined, queryClient) + wrapper: createWrapper(undefined, queryClient), }); await act(() => { const { update } = result.current; diff --git a/scm-ui/ui-types/src/Plugin.ts b/scm-ui/ui-types/src/Plugin.ts index b4d5f6ada9..a9955d27e0 100644 --- a/scm-ui/ui-types/src/Plugin.ts +++ b/scm-ui/ui-types/src/Plugin.ts @@ -24,6 +24,8 @@ import { HalRepresentation, HalRepresentationWithEmbedded } from "./hal"; +type PluginType = "SCM" | "CLOUDOGU"; + export type Plugin = HalRepresentation & { name: string; version: string; @@ -34,6 +36,7 @@ export type Plugin = HalRepresentation & { category: string; avatarUrl?: string; pending: boolean; + type: PluginType; markedForUninstall?: boolean; dependencies: string[]; optionalDependencies: string[]; @@ -43,7 +46,8 @@ export type PluginCollection = HalRepresentationWithEmbedded<{ 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 = { name: string; diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index 0501aa612d..7c2636d6a1 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -47,7 +47,8 @@ "title": { "install": "{{name}} Plugin installieren", "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", "install": "Installieren", @@ -67,6 +68,8 @@ "version": "Version", "currentVersion": "Installierte 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!", "optionalDependencyNotification": "Mit diesem Plugin werden folgende optionale Abhängigkeiten mit aktualisiert, falls sie installiert sind!", "dependencies": "Abhängigkeiten", diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index 6261b51ade..f8adebd13e 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -47,7 +47,8 @@ "title": { "install": "Install {{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", "install": "Install", @@ -67,6 +68,8 @@ "version": "Version", "currentVersion": "Installed 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!", "optionalDependencyNotification": "With this plugin, the following optional dependencies will be updated if they are installed!", "dependencies": "Dependencies", diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx index 003fa075de..02f01b95be 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginEntry.tsx @@ -58,12 +58,18 @@ const PluginEntry: FC = ({ plugin, openModal }) => { const isInstallable = plugin._links.install && (plugin._links.install 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 isCloudoguPlugin = plugin.type === "CLOUDOGU"; const pendingSpinner = () => ( ); const actionBar = () => ( + {isCloudoguPlugin && ( + openModal({ plugin, action: PluginAction.CLOUDOGU })}> + + + )} {isInstallable && ( openModal({ plugin, action: PluginAction.INSTALL })}> diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginGroupEntry.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginGroupEntry.tsx index 7021080343..e47e979bcb 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginGroupEntry.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginGroupEntry.tsx @@ -33,7 +33,7 @@ type Props = { }; const PluginGroupEntry: FC = ({ openModal, group }) => { - const entries = group.plugins.map(plugin => { + const entries = group.plugins.map((plugin) => { return ; }); return ; diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginList.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginList.tsx index feca1f71a4..b409fecd05 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginList.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginList.tsx @@ -36,7 +36,7 @@ const PluginList: FC = ({ plugins, openModal }) => { const groups = groupByCategory(plugins); return (
- {groups.map(group => { + {groups.map((group) => { return ; })}
diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx index d656cc45c3..d200e16206 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx @@ -25,7 +25,7 @@ import React, { FC, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; 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 SuccessNotification from "./SuccessNotification"; import { useInstallPlugin, useUninstallPlugin, useUpdatePlugins } from "@scm-manager/ui-api"; @@ -33,13 +33,17 @@ import { PluginAction } from "../containers/PluginsOverview"; type Props = { plugin: Plugin; - pluginAction: string; + pluginAction: PluginAction; onClose: () => void; }; -const ListParent = styled.div` +type ParentWithPluginAction = { + pluginAction?: PluginAction; +}; + +const ListParent = styled.div.attrs((props) => ({}))` 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; `; @@ -63,9 +67,12 @@ const PluginModal: FC = ({ onClose, pluginAction, plugin }) => { } }, [isDone]); - const handlePluginAction = (e: Event) => { + const handlePluginAction = (e: React.MouseEvent) => { e.preventDefault(); switch (pluginAction) { + case PluginAction.CLOUDOGU: + window.open((plugin._links.cloudoguInstall as Link).href, "_blank"); + break; case PluginAction.INSTALL: install(plugin, { restart: shouldRestart }); break; @@ -76,7 +83,7 @@ const PluginModal: FC = ({ onClose, pluginAction, plugin }) => { update(plugin, { restart: shouldRestart }); break; default: - throw new Error(`Unkown plugin action ${pluginAction}`); + throw new Error(`Unknown plugin action ${pluginAction}`); } }; @@ -172,7 +179,7 @@ const PluginModal: FC = ({ onClose, pluginAction, plugin }) => { disabled={false} /> ); - } else { + } else if (pluginAction !== PluginAction.CLOUDOGU) { return {t("plugins.modal.manualRestartRequired")}; } }; @@ -192,6 +199,13 @@ const PluginModal: FC = ({ onClose, pluginAction, plugin }) => { {plugin.author} + {pluginAction === PluginAction.CLOUDOGU && ( +
+ + {t("plugins.modal.cloudoguInstallInfo")} + +
+ )} {pluginAction === PluginAction.INSTALL && (
@@ -230,7 +244,7 @@ const PluginModal: FC = ({ onClose, pluginAction, plugin }) => { return ( = ({ installed }) => { const { data: availablePlugins, isLoading: isLoadingAvailablePlugins, - error: availablePluginsError + error: availablePluginsError, } = useAvailablePlugins({ enabled: !installed }); const { data: installedPlugins, isLoading: isLoadingInstalledPlugins, - error: installedPluginsError + error: installedPluginsError, } = useInstalledPlugins({ enabled: installed }); const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins(); const [showPendingModal, setShowPendingModal] = useState(false); @@ -166,7 +167,7 @@ const PluginsOverview: FC = ({ installed }) => { const computeUpdateAllSize = () => { const outdatedPlugins = collection?._embedded.plugins.filter((p: Plugin) => p._links.update).length; return t("plugins.outdatedPlugins", { - count: outdatedPlugins + count: outdatedPlugins, }); }; @@ -187,28 +188,14 @@ const PluginsOverview: FC = ({ installed }) => { ); } if (showCancelModal && pendingPlugins) { - return ( - setShowCancelModal(false)} - pendingPlugins={pendingPlugins} - /> - ); + return setShowCancelModal(false)} pendingPlugins={pendingPlugins} />; } if (showUpdateAllModal && collection) { - return ( - setShowUpdateAllModal(false)} - installedPlugins={collection} - /> - ); + return setShowUpdateAllModal(false)} installedPlugins={collection} />; } if (pluginModalContent) { const { action, plugin } = pluginModalContent; - return setPluginModalContent(null)} - /> + return setPluginModalContent(null)} />; } return null; }; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java index c4f11d8a91..66c7d0f90d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java @@ -24,7 +24,6 @@ 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.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -98,6 +97,7 @@ public class AvailablePluginResource { PluginPermissions.read().check(); List installed = pluginManager.getInstalled(); List available = pluginManager.getAvailable().stream().filter(a -> notInstalled(a, installed)).collect(Collectors.toList()); + return Response.ok(collectionMapper.mapAvailable(available)).build(); } 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 282b9860b3..ec4912975a 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 @@ -30,6 +30,7 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import sonia.scm.plugin.PluginInformation; import java.util.Set; @@ -48,6 +49,7 @@ public class PluginDto extends HalRepresentation { private String author; private String category; private String avatarUrl; + private PluginInformation.PluginType type = PluginInformation.PluginType.SCM; private boolean pending; @JsonInclude(JsonInclude.Include.NON_NULL) private Boolean core; 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 ad696b2f36..975926dd68 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 @@ -36,7 +36,6 @@ import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginPermissions; import javax.inject.Inject; - import java.util.List; import java.util.Optional; @@ -71,6 +70,9 @@ public abstract class PluginDtoMapper { PluginDto dto = createDtoForAvailable(plugin); map(dto, plugin); dto.setPending(plugin.isPending()); + if (dto.getType() == null) { + dto.setType(PluginInformation.PluginType.SCM); + } return dto; } @@ -91,8 +93,14 @@ public abstract class PluginDtoMapper { .self(information.getName())); if (!plugin.isPending() && PluginPermissions.write().isPermitted()) { - String href = resourceLinks.availablePlugin().install(information.getName()); - appendLink(links, "install", href); + boolean isCloudoguPlugin = plugin.getDescriptor().getInformation().getType() == PluginInformation.PluginType.CLOUDOGU; + if (isCloudoguPlugin) { + Optional 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()); 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 index cf87b77e8b..234016693e 100644 --- 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 @@ -37,9 +37,9 @@ import javax.ws.rs.Path; @Path("v2/plugins") public class PluginRootResource { - private Provider installedPluginResourceProvider; - private Provider availablePluginResourceProvider; - private Provider pendingPluginResourceProvider; + private final Provider installedPluginResourceProvider; + private final Provider availablePluginResourceProvider; + private final Provider pendingPluginResourceProvider; @Inject public PluginRootResource(Provider installedPluginResourceProvider, Provider availablePluginResourceProvider, Provider pendingPluginResourceProvider) { diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java index c034c98a56..a14e629628 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDto.java @@ -70,26 +70,27 @@ public final class PluginCenterDto implements Serializable { @AllArgsConstructor public static class Plugin { - private String name; - private String version; - private String displayName; - private String description; - private String category; - private String author; - private String avatarUrl; - private String sha256sum; + private final String name; + private final String version; + private final String displayName; + private final String description; + private final String category; + private final String author; + private final String avatarUrl; + private final String sha256sum; + private PluginInformation.PluginType type; @XmlElement(name = "conditions") - private Condition conditions; + private final Condition conditions; @XmlElement(name = "dependencies") - private Set dependencies; + private final Set dependencies; @XmlElement(name = "optionalDependencies") - private Set optionalDependencies; + private final Set optionalDependencies; @XmlElement(name = "_links") - private Map links; + private final Map links; } @XmlAccessorType(XmlAccessType.FIELD) @@ -98,9 +99,9 @@ public final class PluginCenterDto implements Serializable { @AllArgsConstructor public static class Condition { - private List os; - private String arch; - private String minVersion; + private final List os; + private final String arch; + private final String minVersion; } @XmlAccessorType(XmlAccessType.FIELD) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java index 5927592d0c..a996a73941 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterDtoMapper.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; import org.mapstruct.Mapper; @@ -31,22 +31,32 @@ import java.util.HashSet; import java.util.Set; @Mapper -public abstract class PluginCenterDtoMapper { +public abstract class PluginCenterDtoMapper { static final PluginCenterDtoMapper INSTANCE = Mappers.getMapper(PluginCenterDtoMapper.class); abstract PluginInformation map(PluginCenterDto.Plugin plugin); + abstract PluginCondition map(PluginCenterDto.Condition condition); Set map(PluginCenterDto pluginCenterDto) { Set plugins = new HashSet<>(); for (PluginCenterDto.Plugin plugin : pluginCenterDto.getEmbedded().getPlugins()) { String url = plugin.getLinks().get("download").getHref(); + String installLink = getInstallLink(plugin); 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)); } return plugins; } + + private String getInstallLink(PluginCenterDto.Plugin plugin) { + PluginCenterDto.Link link = plugin.getLinks().get("install"); + if (link != null) { + return link.getHref(); + } + return null; + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java index 9a6b51db7a..7e4f687324 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java @@ -44,6 +44,7 @@ import java.net.URI; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; 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.createInstalled; @@ -85,9 +86,14 @@ class PluginDtoMapperTest { assertThat(dto.getAuthor()).isEqualTo("Sebastian Sdorra"); assertThat(dto.getCategory()).isEqualTo("Authentication"); assertThat(dto.getAvatarUrl()).isEqualTo("https://avatar.scm-manager.org/plugins/cas.png"); + assertThat(dto.getType()).isEqualTo(SCM); } private PluginInformation createPluginInformation() { + return createPluginInformation(SCM); + } + + private PluginInformation createPluginInformation(PluginInformation.PluginType type) { PluginInformation information = new PluginInformation(); information.setName("scm-cas-plugin"); information.setVersion("1.0.0"); @@ -95,6 +101,7 @@ class PluginDtoMapperTest { information.setAuthor("Sebastian Sdorra"); information.setCategory("Authentication"); information.setAvatarUrl("https://avatar.scm-manager.org/plugins/cas.png"); + information.setType(type); return information; } @@ -135,6 +142,18 @@ class PluginDtoMapperTest { .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 void shouldAppendInstallWithRestartLink() { when(restarter.isSupported()).thenReturn(true); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java index 96ceccfb54..abd6277489 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterDtoMapperTest.java @@ -58,13 +58,14 @@ class PluginCenterDtoMapperTest { void shouldMapSinglePlugin() { Plugin plugin = new Plugin( "scm-hitchhiker-plugin", + "2.0.0", "SCM Hitchhiker Plugin", "plugin for hitchhikers", "Travel", - "2.0.0", "trillian", "http://avatar.url", "555000444", + PluginInformation.PluginType.SCM, new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), ImmutableSet.of("scm-review-plugin"), ImmutableSet.of(), @@ -93,13 +94,14 @@ class PluginCenterDtoMapperTest { void shouldMapMultiplePlugins() { Plugin plugin1 = new Plugin( "scm-review-plugin", + "2.1.0", "SCM Hitchhiker Plugin", "plugin for hitchhikers", "Travel", - "2.1.0", "trillian", "https://avatar.url", "12345678aa", + PluginInformation.PluginType.SCM, new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), ImmutableSet.of("scm-review-plugin"), ImmutableSet.of(), @@ -108,13 +110,14 @@ class PluginCenterDtoMapperTest { Plugin plugin2 = new Plugin( "scm-hitchhiker-plugin", + "2.0.0", "SCM Hitchhiker Plugin", "plugin for hitchhikers", "Travel", - "2.0.0", "dent", "http://avatar.url", "555000444", + PluginInformation.PluginType.CLOUDOGU, new Condition(Collections.singletonList("linux"), "amd64","2.0.0"), ImmutableSet.of("scm-review-plugin"), ImmutableSet.of(), @@ -132,6 +135,8 @@ class PluginCenterDtoMapperTest { assertThat(pluginInformation1.getVersion()).isEqualTo(plugin1.getVersion()); assertThat(pluginInformation2.getAuthor()).isEqualTo(plugin2.getAuthor()); 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); } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java index 548888d6df..ef2c2a9947 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java @@ -26,6 +26,8 @@ package sonia.scm.plugin; import org.mockito.Answers; +import java.util.Optional; + import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -62,6 +64,7 @@ public class PluginTestHelper { public static AvailablePlugin createAvailable(PluginInformation information) { AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); lenient().when(descriptor.getInformation()).thenReturn(information); + lenient().when(descriptor.getInstallLink()).thenReturn(Optional.of("mycloudogu.com/install/my_plugin")); return new AvailablePlugin(descriptor); }