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 { } render() { - const { loading, installed, t } = this.props; + const { loading, error, collection, installed, t } = this.props; - if (loading) { + if (error) { + return ; + } + + if (!collection || loading) { return ; } @@ -58,27 +68,21 @@ class PluginsOverview extends React.Component { } renderPluginsList() { - const { collection, page, t } = this.props; + const { collection, t } = this.props; if (collection._embedded && collection._embedded.plugins.length > 0) { - return ( - <> - - - - ); + return ; } - return ( - {t("plugins.noPlugins")} - ); + return {t("plugins.noPlugins")}; } } -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 + ); + }); +});