mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 08:55:44 +01:00
Introduce stale while revalidate pattern (#1555)
This Improves the frontend performance with stale while revalidate pattern. There are noticeable performance problems in the frontend that needed addressing. While implementing the stale-while-revalidate pattern to display cached responses while re-fetching up-to-date data in the background, in the same vein we used the opportunity to remove legacy code involving redux as much as possible, cleaned up many components and converted them to functional react components. Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
committed by
GitHub
parent
ad5c8102c0
commit
3a8d031ed5
@@ -21,40 +21,39 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import PluginActionModal from "./PluginActionModal";
|
||||
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCancelPendingPlugins } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
refresh: () => void;
|
||||
pendingPlugins: PendingPlugins;
|
||||
};
|
||||
|
||||
class CancelPendingActionModal extends React.Component<Props> {
|
||||
render() {
|
||||
const { onClose, pendingPlugins, t } = this.props;
|
||||
const CancelPendingActionModal: FC<Props> = ({ onClose, pendingPlugins }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
const { error, isLoading, update, isCancelled } = useCancelPendingPlugins();
|
||||
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.cancelPending")}
|
||||
label={t("plugins.cancelPending")}
|
||||
onClose={onClose}
|
||||
pendingPlugins={pendingPlugins}
|
||||
execute={this.cancelPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isCancelled) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose, isCancelled]);
|
||||
|
||||
cancelPending = () => {
|
||||
const { pendingPlugins, refresh, onClose } = this.props;
|
||||
return apiClient
|
||||
.post(pendingPlugins._links.cancel.href)
|
||||
.then(refresh)
|
||||
.then(onClose);
|
||||
};
|
||||
}
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.cancelPending")}
|
||||
label={t("plugins.cancelPending")}
|
||||
onClose={onClose}
|
||||
pendingPlugins={pendingPlugins}
|
||||
success={isCancelled}
|
||||
loading={isLoading}
|
||||
error={error}
|
||||
execute={() => update(pendingPlugins)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation("admin")(CancelPendingActionModal);
|
||||
export default CancelPendingActionModal;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import { Button } from "@scm-manager/ui-components";
|
||||
import ExecutePendingModal from "./ExecutePendingModal";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
pendingPlugins: PendingPlugins;
|
||||
};
|
||||
|
||||
type State = {
|
||||
showModal: boolean;
|
||||
};
|
||||
|
||||
class ExecutePendingAction extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showModal: false
|
||||
};
|
||||
}
|
||||
|
||||
openModal = () => {
|
||||
this.setState({
|
||||
showModal: true
|
||||
});
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
this.setState({
|
||||
showModal: false
|
||||
});
|
||||
};
|
||||
|
||||
renderModal = () => {
|
||||
const { showModal } = this.state;
|
||||
const { pendingPlugins } = this.props;
|
||||
if (showModal) {
|
||||
return <ExecutePendingModal pendingPlugins={pendingPlugins} onClose={this.closeModal} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<>
|
||||
{this.renderModal()}
|
||||
<Button color="primary" label={t("plugins.executePending")} action={this.openModal} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("admin")(ExecutePendingAction);
|
||||
@@ -21,39 +21,36 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import { apiClient, Notification } from "@scm-manager/ui-components";
|
||||
import waitForRestart from "./waitForRestart";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
import PluginActionModal from "./PluginActionModal";
|
||||
import { useExecutePendingPlugins } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
pendingPlugins: PendingPlugins;
|
||||
};
|
||||
|
||||
class ExecutePendingActionModal extends React.Component<Props> {
|
||||
render() {
|
||||
const { onClose, pendingPlugins, t } = this.props;
|
||||
const ExecutePendingActionModal: FC<Props> = ({ onClose, pendingPlugins }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
const { update, isLoading: loading, error, isExecuted } = useExecutePendingPlugins();
|
||||
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.executePending")}
|
||||
label={t("plugins.modal.executeAndRestart")}
|
||||
onClose={onClose}
|
||||
pendingPlugins={pendingPlugins}
|
||||
execute={this.executeAndRestart}
|
||||
>
|
||||
<Notification type="warning">{t("plugins.modal.restartNotification")}</Notification>
|
||||
</PluginActionModal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.executePending")}
|
||||
label={t("plugins.modal.executeAndRestart")}
|
||||
onClose={onClose}
|
||||
pendingPlugins={pendingPlugins}
|
||||
error={error}
|
||||
loading={loading}
|
||||
success={isExecuted}
|
||||
execute={() => update(pendingPlugins)}
|
||||
>
|
||||
<Notification type="warning">{t("plugins.modal.restartNotification")}</Notification>
|
||||
</PluginActionModal>
|
||||
);
|
||||
};
|
||||
|
||||
executeAndRestart = () => {
|
||||
const { pendingPlugins } = this.props;
|
||||
return apiClient.post(pendingPlugins._links.execute.href).then(waitForRestart);
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("admin")(ExecutePendingActionModal);
|
||||
export default ExecutePendingActionModal;
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { apiClient, Button, ButtonGroup, ErrorNotification, Modal, Notification } from "@scm-manager/ui-components";
|
||||
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import waitForRestart from "./waitForRestart";
|
||||
import SuccessNotification from "./SuccessNotification";
|
||||
import PendingPluginsQueue from "./PendingPluginsQueue";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
onClose: () => void;
|
||||
pendingPlugins: PendingPlugins;
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
success: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
class ExecutePendingModal extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
success: false
|
||||
};
|
||||
}
|
||||
|
||||
renderNotifications = () => {
|
||||
const { t } = this.props;
|
||||
const { error, success } = this.state;
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
} else if (success) {
|
||||
return <SuccessNotification />;
|
||||
} else {
|
||||
return <Notification type="warning">{t("plugins.modal.restartNotification")}</Notification>;
|
||||
}
|
||||
};
|
||||
|
||||
executeAndRestart = () => {
|
||||
const { pendingPlugins } = this.props;
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
apiClient
|
||||
.post(pendingPlugins._links.execute.href)
|
||||
.then(waitForRestart)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
success: true,
|
||||
loading: false,
|
||||
error: undefined
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
success: false,
|
||||
loading: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
renderBody = () => {
|
||||
const { pendingPlugins, t } = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="media">
|
||||
<div className="content">
|
||||
<p>{t("plugins.modal.executePending")}</p>
|
||||
<PendingPluginsQueue pendingPlugins={pendingPlugins} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="media">{this.renderNotifications()}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const { onClose, t } = this.props;
|
||||
const { loading, error, success } = this.state;
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="warning"
|
||||
label={t("plugins.modal.executeAndRestart")}
|
||||
loading={loading}
|
||||
action={this.executeAndRestart}
|
||||
disabled={error || success}
|
||||
/>
|
||||
<Button label={t("plugins.modal.abort")} action={onClose} />
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onClose, t } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
title={t("plugins.modal.executeAndRestart")}
|
||||
closeFunction={onClose}
|
||||
body={this.renderBody()}
|
||||
footer={this.renderFooter()}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("admin")(ExecutePendingModal);
|
||||
@@ -29,35 +29,20 @@ import SuccessNotification from "./SuccessNotification";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
onClose: () => void;
|
||||
actionType: string;
|
||||
pendingPlugins?: PendingPlugins;
|
||||
installedPlugins?: PluginCollection;
|
||||
refresh: () => void;
|
||||
execute: () => Promise<any>;
|
||||
execute: () => void;
|
||||
description: string;
|
||||
label: string;
|
||||
|
||||
loading: boolean;
|
||||
error?: Error | null;
|
||||
success: boolean;
|
||||
children?: React.Node;
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
success: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
class PluginActionModal extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
success: false
|
||||
};
|
||||
}
|
||||
|
||||
class PluginActionModal extends React.Component<Props> {
|
||||
renderNotifications = () => {
|
||||
const { children } = this.props;
|
||||
const { error, success } = this.state;
|
||||
const { children, error, success } = this.props;
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
} else if (success) {
|
||||
@@ -67,28 +52,6 @@ class PluginActionModal extends React.Component<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
executeAction = () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
this.props
|
||||
.execute()
|
||||
.then(() => {
|
||||
this.setState({
|
||||
success: true,
|
||||
loading: false
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
success: false,
|
||||
loading: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
renderModalContent = () => {
|
||||
return (
|
||||
<>
|
||||
@@ -189,16 +152,15 @@ class PluginActionModal extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const { onClose, t } = this.props;
|
||||
const { loading, error, success } = this.state;
|
||||
const { onClose, t, loading, error, success } = this.props;
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="warning"
|
||||
label={this.props.label}
|
||||
loading={loading}
|
||||
action={this.executeAction}
|
||||
disabled={error || success}
|
||||
action={this.props.execute}
|
||||
disabled={!!error || success}
|
||||
/>
|
||||
<Button label={t("plugins.modal.abort")} action={onClose} />
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -21,29 +21,17 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { Plugin } from "@scm-manager/ui-types";
|
||||
import { Link, Plugin } from "@scm-manager/ui-types";
|
||||
import { CardColumn, Icon } from "@scm-manager/ui-components";
|
||||
import PluginAvatar from "./PluginAvatar";
|
||||
import PluginModal from "./PluginModal";
|
||||
import { PluginAction, PluginModalContent } from "../containers/PluginsOverview";
|
||||
|
||||
export const PluginAction = {
|
||||
INSTALL: "install",
|
||||
UPDATE: "update",
|
||||
UNINSTALL: "uninstall"
|
||||
};
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
plugin: Plugin;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
showInstallModal: boolean;
|
||||
showUpdateModal: boolean;
|
||||
showUninstallModal: boolean;
|
||||
openModal: (content: PluginModalContent) => void;
|
||||
};
|
||||
|
||||
const ActionbarWrapper = styled.div`
|
||||
@@ -65,136 +53,48 @@ const IconWrapper = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
class PluginEntry extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const PluginEntry: FC<Props> = ({ plugin, openModal }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
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;
|
||||
|
||||
this.state = {
|
||||
showInstallModal: false,
|
||||
showUpdateModal: false,
|
||||
showUninstallModal: false
|
||||
};
|
||||
}
|
||||
const pendingSpinner = () => (
|
||||
<Icon className="fa-spin fa-lg" name="spinner" color={plugin.markedForUninstall ? "danger" : "info"} />
|
||||
);
|
||||
const actionBar = () => (
|
||||
<ActionbarWrapper className="is-flex">
|
||||
{isInstallable && (
|
||||
<IconWrapper className="level-item" onClick={() => openModal({ plugin, action: PluginAction.INSTALL })}>
|
||||
<Icon title={t("plugins.modal.install")} name="download" color="info" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{isUninstallable && (
|
||||
<IconWrapper className="level-item" onClick={() => openModal({ plugin, action: PluginAction.UNINSTALL })}>
|
||||
<Icon title={t("plugins.modal.uninstall")} name="trash" color="info" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{isUpdatable && (
|
||||
<IconWrapper className="level-item" onClick={() => openModal({ plugin, action: PluginAction.UPDATE })}>
|
||||
<Icon title={t("plugins.modal.update")} name="sync-alt" color="info" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
</ActionbarWrapper>
|
||||
);
|
||||
|
||||
createAvatar = (plugin: Plugin) => {
|
||||
return <PluginAvatar plugin={plugin} />;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<CardColumn
|
||||
action={isInstallable ? () => openModal({ plugin, action: PluginAction.INSTALL }) : undefined}
|
||||
avatar={<PluginAvatar plugin={plugin} />}
|
||||
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
|
||||
description={plugin.description}
|
||||
contentRight={plugin.pending || plugin.markedForUninstall ? pendingSpinner() : actionBar()}
|
||||
footerLeft={<small>{plugin.version}</small>}
|
||||
footerRight={<small className="level-item is-block shorten-text">{plugin.author}</small>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
toggleModal = (showModal: string) => {
|
||||
const oldValue = this.state[showModal];
|
||||
this.setState({
|
||||
[showModal]: !oldValue
|
||||
});
|
||||
};
|
||||
|
||||
createFooterLeft = (plugin: Plugin) => {
|
||||
return <small>{plugin.version}</small>;
|
||||
};
|
||||
|
||||
createFooterRight = (plugin: Plugin) => {
|
||||
return <small className="level-item is-block shorten-text">{plugin.author}</small>;
|
||||
};
|
||||
|
||||
isInstallable = () => {
|
||||
const { plugin } = this.props;
|
||||
return plugin._links && plugin._links.install && plugin._links.install.href;
|
||||
};
|
||||
|
||||
isUpdatable = () => {
|
||||
const { plugin } = this.props;
|
||||
return plugin._links && plugin._links.update && plugin._links.update.href;
|
||||
};
|
||||
|
||||
isUninstallable = () => {
|
||||
const { plugin } = this.props;
|
||||
return plugin._links && plugin._links.uninstall && plugin._links.uninstall.href;
|
||||
};
|
||||
|
||||
createActionbar = () => {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<ActionbarWrapper className="is-flex">
|
||||
{this.isInstallable() && (
|
||||
<IconWrapper className="level-item" onClick={() => this.toggleModal("showInstallModal")}>
|
||||
<Icon title={t("plugins.modal.install")} name="download" color="info" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{this.isUninstallable() && (
|
||||
<IconWrapper className="level-item" onClick={() => this.toggleModal("showUninstallModal")}>
|
||||
<Icon title={t("plugins.modal.uninstall")} name="trash" color="info" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{this.isUpdatable() && (
|
||||
<IconWrapper className="level-item" onClick={() => this.toggleModal("showUpdateModal")}>
|
||||
<Icon title={t("plugins.modal.update")} name="sync-alt" color="info" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
</ActionbarWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
renderModal = () => {
|
||||
const { plugin, refresh } = this.props;
|
||||
if (this.state.showInstallModal && this.isInstallable()) {
|
||||
return (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={PluginAction.INSTALL}
|
||||
refresh={refresh}
|
||||
onClose={() => this.toggleModal("showInstallModal")}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.showUpdateModal && this.isUpdatable()) {
|
||||
return (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={PluginAction.UPDATE}
|
||||
refresh={refresh}
|
||||
onClose={() => this.toggleModal("showUpdateModal")}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.showUninstallModal && this.isUninstallable()) {
|
||||
return (
|
||||
<PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={PluginAction.UNINSTALL}
|
||||
refresh={refresh}
|
||||
onClose={() => this.toggleModal("showUninstallModal")}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
createPendingSpinner = () => {
|
||||
const { plugin } = this.props;
|
||||
return <Icon className="fa-spin fa-lg" name="spinner" color={plugin.markedForUninstall ? "danger" : "info"} />;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { plugin } = this.props;
|
||||
const avatar = this.createAvatar(plugin);
|
||||
const actionbar = this.createActionbar();
|
||||
const footerLeft = this.createFooterLeft(plugin);
|
||||
const footerRight = this.createFooterRight(plugin);
|
||||
const modal = this.renderModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardColumn
|
||||
action={this.isInstallable() ? () => this.toggleModal("showInstallModal") : null}
|
||||
avatar={avatar}
|
||||
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
|
||||
description={plugin.description}
|
||||
contentRight={plugin.pending || plugin.markedForUninstall ? this.createPendingSpinner() : actionbar}
|
||||
footerLeft={footerLeft}
|
||||
footerRight={footerRight}
|
||||
/>
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("admin")(PluginEntry);
|
||||
export default PluginEntry;
|
||||
|
||||
@@ -21,24 +21,22 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import { CardColumnGroup } from "@scm-manager/ui-components";
|
||||
import { PluginGroup } from "@scm-manager/ui-types";
|
||||
import PluginEntry from "./PluginEntry";
|
||||
import { PluginModalContent } from "../containers/PluginsOverview";
|
||||
|
||||
type Props = {
|
||||
group: PluginGroup;
|
||||
refresh: () => void;
|
||||
openModal: (content: PluginModalContent) => void;
|
||||
};
|
||||
|
||||
class PluginGroupEntry extends React.Component<Props> {
|
||||
render() {
|
||||
const { group, refresh } = this.props;
|
||||
const entries = group.plugins.map(plugin => {
|
||||
return <PluginEntry plugin={plugin} key={plugin.name} refresh={refresh} />;
|
||||
});
|
||||
return <CardColumnGroup name={group.name} elements={entries} />;
|
||||
}
|
||||
}
|
||||
const PluginGroupEntry: FC<Props> = ({ openModal, group }) => {
|
||||
const entries = group.plugins.map(plugin => {
|
||||
return <PluginEntry plugin={plugin} openModal={openModal} key={plugin.name} />;
|
||||
});
|
||||
return <CardColumnGroup name={group.name} elements={entries} />;
|
||||
};
|
||||
|
||||
export default PluginGroupEntry;
|
||||
|
||||
@@ -21,29 +21,26 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import { Plugin } from "@scm-manager/ui-types";
|
||||
import PluginGroupEntry from "../components/PluginGroupEntry";
|
||||
import groupByCategory from "./groupByCategory";
|
||||
import { PluginModalContent } from "../containers/PluginsOverview";
|
||||
|
||||
type Props = {
|
||||
plugins: Plugin[];
|
||||
refresh: () => void;
|
||||
openModal: (content: PluginModalContent) => void;
|
||||
};
|
||||
|
||||
class PluginList extends React.Component<Props> {
|
||||
render() {
|
||||
const { plugins, refresh } = this.props;
|
||||
|
||||
const groups = groupByCategory(plugins);
|
||||
return (
|
||||
<div className="content is-plugin-page">
|
||||
{groups.map(group => {
|
||||
return <PluginGroupEntry group={group} key={group.name} refresh={refresh} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const PluginList: FC<Props> = ({ plugins, openModal }) => {
|
||||
const groups = groupByCategory(plugins);
|
||||
return (
|
||||
<div className="content is-plugin-page">
|
||||
{groups.map(group => {
|
||||
return <PluginGroupEntry group={group} openModal={openModal} key={group.name} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginList;
|
||||
|
||||
@@ -21,38 +21,22 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
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 {
|
||||
apiClient,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Checkbox,
|
||||
ErrorNotification,
|
||||
Modal,
|
||||
Notification
|
||||
} from "@scm-manager/ui-components";
|
||||
import waitForRestart from "./waitForRestart";
|
||||
import { Button, ButtonGroup, Checkbox, ErrorNotification, Modal, Notification } from "@scm-manager/ui-components";
|
||||
import SuccessNotification from "./SuccessNotification";
|
||||
import { PluginAction } from "./PluginEntry";
|
||||
import { useInstallPlugin, useUninstallPlugin, useUpdatePlugins } from "@scm-manager/ui-api";
|
||||
import { PluginAction } from "../containers/PluginsOverview";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
plugin: Plugin;
|
||||
pluginAction: string;
|
||||
refresh: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
success: boolean;
|
||||
restart: boolean;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
const ListParent = styled.div`
|
||||
margin-right: 0;
|
||||
min-width: ${props => (props.pluginAction === PluginAction.INSTALL ? "5.5em" : "10em")};
|
||||
@@ -63,88 +47,43 @@ const ListChild = styled.div`
|
||||
flex-grow: 4;
|
||||
`;
|
||||
|
||||
class PluginModal extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
restart: false,
|
||||
success: false
|
||||
};
|
||||
}
|
||||
const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
const [shouldRestart, setShouldRestart] = useState<boolean>(false);
|
||||
const { isLoading: isInstalling, error: installError, install, isInstalled } = useInstallPlugin();
|
||||
const { isLoading: isUninstalling, error: uninstallError, uninstall, isUninstalled } = useUninstallPlugin();
|
||||
const { isLoading: isUpdating, error: updateError, update, isUpdated } = useUpdatePlugins();
|
||||
const error = installError || uninstallError || updateError;
|
||||
const loading = isInstalling || isUninstalling || isUpdating;
|
||||
const isDone = isInstalled || isUninstalled || isUpdated;
|
||||
|
||||
onSuccess = () => {
|
||||
const { restart } = this.state;
|
||||
const { refresh, onClose } = this.props;
|
||||
|
||||
const newState = {
|
||||
loading: false,
|
||||
error: undefined
|
||||
};
|
||||
|
||||
if (restart) {
|
||||
waitForRestart()
|
||||
.then(() => {
|
||||
this.setState({
|
||||
...newState,
|
||||
success: true
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
success: false,
|
||||
error
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.setState(newState, () => {
|
||||
refresh();
|
||||
onClose();
|
||||
});
|
||||
useEffect(() => {
|
||||
if (isDone && !shouldRestart) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
}, [isDone]);
|
||||
|
||||
createPluginActionLink = () => {
|
||||
const { plugin, pluginAction } = this.props;
|
||||
const { restart } = this.state;
|
||||
|
||||
let pluginActionLink = "";
|
||||
|
||||
if (pluginAction === PluginAction.INSTALL) {
|
||||
pluginActionLink = plugin._links.install.href;
|
||||
} else if (pluginAction === PluginAction.UPDATE) {
|
||||
pluginActionLink = plugin._links.update.href;
|
||||
} else if (pluginAction === PluginAction.UNINSTALL) {
|
||||
pluginActionLink = plugin._links.uninstall.href;
|
||||
}
|
||||
return pluginActionLink + "?restart=" + restart.toString();
|
||||
};
|
||||
|
||||
handlePluginAction = (e: Event) => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
const handlePluginAction = (e: Event) => {
|
||||
e.preventDefault();
|
||||
apiClient
|
||||
.post(this.createPluginActionLink())
|
||||
.then(this.onSuccess)
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
switch (pluginAction) {
|
||||
case PluginAction.INSTALL:
|
||||
install(plugin, { restart: shouldRestart });
|
||||
break;
|
||||
case PluginAction.UNINSTALL:
|
||||
uninstall(plugin, { restart: shouldRestart });
|
||||
break;
|
||||
case PluginAction.UPDATE:
|
||||
update(plugin, { restart: shouldRestart });
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unkown plugin action ${pluginAction}`);
|
||||
}
|
||||
};
|
||||
|
||||
footer = () => {
|
||||
const { pluginAction, onClose, t } = this.props;
|
||||
const { loading, error, restart, success } = this.state;
|
||||
|
||||
const footer = () => {
|
||||
let color = pluginAction === PluginAction.UNINSTALL ? "warning" : "primary";
|
||||
let label = `plugins.modal.${pluginAction}`;
|
||||
if (restart) {
|
||||
if (shouldRestart) {
|
||||
color = "warning";
|
||||
label = `plugins.modal.${pluginAction}AndRestart`;
|
||||
}
|
||||
@@ -153,18 +92,16 @@ class PluginModal extends React.Component<Props, State> {
|
||||
<Button
|
||||
label={t(label)}
|
||||
color={color}
|
||||
action={this.handlePluginAction}
|
||||
action={handlePluginAction}
|
||||
loading={loading}
|
||||
disabled={!!error || success}
|
||||
disabled={!!error || isDone}
|
||||
/>
|
||||
<Button label={t("plugins.modal.abort")} action={onClose} />
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
renderDependencies() {
|
||||
const { plugin, t } = this.props;
|
||||
|
||||
const renderDependencies = () => {
|
||||
let dependencies = null;
|
||||
if (plugin.dependencies && plugin.dependencies.length > 0) {
|
||||
dependencies = (
|
||||
@@ -181,11 +118,9 @@ class PluginModal extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
renderOptionalDependencies() {
|
||||
const { plugin, t } = this.props;
|
||||
};
|
||||
|
||||
const renderOptionalDependencies = () => {
|
||||
let optionalDependencies = null;
|
||||
if (plugin.optionalDependencies && plugin.optionalDependencies.length > 0) {
|
||||
optionalDependencies = (
|
||||
@@ -202,24 +137,22 @@ class PluginModal extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
return optionalDependencies;
|
||||
}
|
||||
};
|
||||
|
||||
renderNotifications = () => {
|
||||
const { t, pluginAction } = this.props;
|
||||
const { restart, error, success } = this.state;
|
||||
const renderNotifications = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="media">
|
||||
<ErrorNotification error={error} />
|
||||
</div>
|
||||
);
|
||||
} else if (success) {
|
||||
} else if (isDone && shouldRestart) {
|
||||
return (
|
||||
<div className="media">
|
||||
<SuccessNotification pluginAction={pluginAction} />
|
||||
</div>
|
||||
);
|
||||
} else if (restart) {
|
||||
} else if (shouldRestart) {
|
||||
return (
|
||||
<div className="media">
|
||||
<Notification type="warning">{t("plugins.modal.restartNotification")}</Notification>
|
||||
@@ -229,22 +162,13 @@ class PluginModal extends React.Component<Props, State> {
|
||||
return null;
|
||||
};
|
||||
|
||||
handleRestartChange = (value: boolean) => {
|
||||
this.setState({
|
||||
restart: value
|
||||
});
|
||||
};
|
||||
|
||||
createRestartSectionContent = () => {
|
||||
const { restart } = this.state;
|
||||
const { plugin, pluginAction, t } = this.props;
|
||||
|
||||
const createRestartSectionContent = () => {
|
||||
if (plugin._links[pluginAction + "WithRestart"]) {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={restart}
|
||||
checked={shouldRestart}
|
||||
label={t("plugins.modal.restart")}
|
||||
onChange={this.handleRestartChange}
|
||||
onChange={setShouldRestart}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
@@ -253,71 +177,67 @@ class PluginModal extends React.Component<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { plugin, pluginAction, onClose, t } = this.props;
|
||||
|
||||
const body = (
|
||||
<>
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<p>{plugin.description}</p>
|
||||
</div>
|
||||
const body = (
|
||||
<>
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<p>{plugin.description}</p>
|
||||
</div>
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
</div>
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")} pluginAction={pluginAction}>
|
||||
{t("plugins.modal.author")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.author}</ListChild>
|
||||
</div>
|
||||
{pluginAction === PluginAction.INSTALL && (
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")} pluginAction={pluginAction}>
|
||||
{t("plugins.modal.author")}:
|
||||
{t("plugins.modal.version")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.author}</ListChild>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.version}</ListChild>
|
||||
</div>
|
||||
{pluginAction === PluginAction.INSTALL && (
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")} pluginAction={pluginAction}>
|
||||
{t("plugins.modal.version")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.version}</ListChild>
|
||||
</div>
|
||||
)}
|
||||
{(pluginAction === PluginAction.UPDATE || pluginAction === PluginAction.UNINSTALL) && (
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")}>
|
||||
{t("plugins.modal.currentVersion")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.version}</ListChild>
|
||||
</div>
|
||||
)}
|
||||
{pluginAction === PluginAction.UPDATE && (
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")}>
|
||||
{t("plugins.modal.newVersion")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.newVersion}</ListChild>
|
||||
</div>
|
||||
)}
|
||||
{this.renderDependencies()}
|
||||
{this.renderOptionalDependencies()}
|
||||
</div>
|
||||
)}
|
||||
{(pluginAction === PluginAction.UPDATE || pluginAction === PluginAction.UNINSTALL) && (
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")}>
|
||||
{t("plugins.modal.currentVersion")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.version}</ListChild>
|
||||
</div>
|
||||
)}
|
||||
{pluginAction === PluginAction.UPDATE && (
|
||||
<div className="field is-horizontal">
|
||||
<ListParent className={classNames("field-label", "is-inline-flex")}>
|
||||
{t("plugins.modal.newVersion")}:
|
||||
</ListParent>
|
||||
<ListChild className={classNames("field-body", "is-inline-flex")}>{plugin.newVersion}</ListChild>
|
||||
</div>
|
||||
)}
|
||||
{renderDependencies()}
|
||||
{renderOptionalDependencies()}
|
||||
</div>
|
||||
<div className="media">
|
||||
<div className="media-content">{this.createRestartSectionContent()}</div>
|
||||
</div>
|
||||
{this.renderNotifications()}
|
||||
</>
|
||||
);
|
||||
</div>
|
||||
<div className="media">
|
||||
<div className="media-content">{createRestartSectionContent()}</div>
|
||||
</div>
|
||||
{renderNotifications()}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`plugins.modal.title.${pluginAction}`, {
|
||||
name: plugin.displayName ? plugin.displayName : plugin.name
|
||||
})}
|
||||
closeFunction={() => onClose()}
|
||||
body={body}
|
||||
footer={this.footer()}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title={t(`plugins.modal.title.${pluginAction}`, {
|
||||
name: plugin.displayName ? plugin.displayName : plugin.name
|
||||
})}
|
||||
closeFunction={onClose}
|
||||
body={body}
|
||||
footer={footer()}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default withTranslation("admin")(PluginModal);
|
||||
export default PluginModal;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Notification } from "@scm-manager/ui-components";
|
||||
import { PluginAction } from "./PluginEntry";
|
||||
import { PluginAction } from "../containers/PluginsOverview";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
pluginAction?: string;
|
||||
@@ -48,7 +48,7 @@ class InstallSuccessNotification extends React.Component<Props> {
|
||||
return (
|
||||
<Notification type="success">
|
||||
{this.createMessageForPluginAction()}{" "}
|
||||
<a onClick={e => window.location.reload(true)}>{t("plugins.modal.reload")}</a>
|
||||
<a onClick={_ => window.location.reload(true)}>{t("plugins.modal.reload")}</a>
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,40 +21,38 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PluginCollection } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import PluginActionModal from "./PluginActionModal";
|
||||
import { useUpdatePlugins } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
refresh: () => void;
|
||||
installedPlugins: PluginCollection;
|
||||
};
|
||||
|
||||
class UpdateAllActionModal extends React.Component<Props> {
|
||||
render() {
|
||||
const { onClose, installedPlugins, t } = this.props;
|
||||
const UpdateAllActionModal: FC<Props> = ({ installedPlugins, onClose }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
const { update, isLoading, error, isUpdated } = useUpdatePlugins();
|
||||
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.updateAll")}
|
||||
label={t("plugins.updateAll")}
|
||||
onClose={onClose}
|
||||
installedPlugins={installedPlugins}
|
||||
execute={this.updateAll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isUpdated) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose, isUpdated]);
|
||||
|
||||
updateAll = () => {
|
||||
const { installedPlugins, refresh, onClose } = this.props;
|
||||
return apiClient
|
||||
.post(installedPlugins._links.update.href)
|
||||
.then(refresh)
|
||||
.then(onClose);
|
||||
};
|
||||
}
|
||||
|
||||
export default withTranslation("admin")(UpdateAllActionModal);
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.updateAll")}
|
||||
label={t("plugins.updateAll")}
|
||||
onClose={onClose}
|
||||
installedPlugins={installedPlugins}
|
||||
error={error}
|
||||
loading={isLoading}
|
||||
success={isUpdated}
|
||||
execute={() => update(installedPlugins)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default UpdateAllActionModal;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
|
||||
const waitForRestart = () => {
|
||||
const endTime = Number(new Date()) + 60000;
|
||||
let started = false;
|
||||
|
||||
const executor = (resolve, reject) => {
|
||||
// we need some initial delay
|
||||
if (!started) {
|
||||
started = true;
|
||||
setTimeout(executor, 1000, resolve, reject);
|
||||
} else {
|
||||
apiClient
|
||||
.get("")
|
||||
.then(resolve)
|
||||
.catch(() => {
|
||||
if (Number(new Date()) < endTime) {
|
||||
setTimeout(executor, 500, resolve, reject);
|
||||
} else {
|
||||
reject(new Error("timeout reached"));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise<void>(executor);
|
||||
};
|
||||
|
||||
export default waitForRestart;
|
||||
@@ -22,10 +22,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { compose } from "redux";
|
||||
import { PendingPlugins, Plugin, PluginCollection } from "@scm-manager/ui-types";
|
||||
import { FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plugin } from "@scm-manager/ui-types";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -35,89 +34,54 @@ import {
|
||||
Subtitle,
|
||||
Title
|
||||
} from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchPendingPlugins,
|
||||
fetchPluginsByLink,
|
||||
getFetchPluginsFailure,
|
||||
getPendingPlugins,
|
||||
getPluginCollection,
|
||||
isFetchPluginsPending
|
||||
} from "../modules/plugins";
|
||||
import PluginsList from "../components/PluginList";
|
||||
import {
|
||||
getPendingPluginsLink,
|
||||
mustGetAvailablePluginsLink,
|
||||
mustGetInstalledPluginsLink
|
||||
} from "../../../modules/indexResource";
|
||||
import PluginTopActions from "../components/PluginTopActions";
|
||||
import PluginBottomActions from "../components/PluginBottomActions";
|
||||
import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
|
||||
import CancelPendingActionModal from "../components/CancelPendingActionModal";
|
||||
import UpdateAllActionModal from "../components/UpdateAllActionModal";
|
||||
import ShowPendingModal from "../components/ShowPendingModal";
|
||||
import { useAvailablePlugins, useInstalledPlugins, usePendingPlugins } from "@scm-manager/ui-api";
|
||||
import PluginModal from "../components/PluginModal";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
collection: PluginCollection;
|
||||
baseUrl: string;
|
||||
export enum PluginAction {
|
||||
INSTALL = "install",
|
||||
UPDATE = "update",
|
||||
UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
export type PluginModalContent = {
|
||||
plugin: Plugin;
|
||||
action: PluginAction;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
installed: boolean;
|
||||
availablePluginsLink: string;
|
||||
installedPluginsLink: string;
|
||||
pendingPluginsLink: string;
|
||||
pendingPlugins: PendingPlugins;
|
||||
|
||||
// dispatched functions
|
||||
fetchPluginsByLink: (link: string) => void;
|
||||
fetchPendingPlugins: (link: string) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
showPendingModal: boolean;
|
||||
showExecutePendingModal: boolean;
|
||||
showUpdateAllModal: boolean;
|
||||
showCancelModal: boolean;
|
||||
};
|
||||
const PluginsOverview: FC<Props> = ({ installed }) => {
|
||||
const [t] = useTranslation("admin");
|
||||
const {
|
||||
data: availablePlugins,
|
||||
isLoading: isLoadingAvailablePlugins,
|
||||
error: availablePluginsError
|
||||
} = useAvailablePlugins({ enabled: !installed });
|
||||
const {
|
||||
data: installedPlugins,
|
||||
isLoading: isLoadingInstalledPlugins,
|
||||
error: installedPluginsError
|
||||
} = useInstalledPlugins({ enabled: installed });
|
||||
const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins();
|
||||
const [showPendingModal, setShowPendingModal] = useState(false);
|
||||
const [showExecutePendingModal, setShowExecutePendingModal] = useState(false);
|
||||
const [showUpdateAllModal, setShowUpdateAllModal] = useState(false);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
const [pluginModalContent, setPluginModalContent] = useState<PluginModalContent | null>(null);
|
||||
const collection = installed ? installedPlugins : availablePlugins;
|
||||
const error = (installed ? installedPluginsError : availablePluginsError) || pendingPluginsError;
|
||||
const loading = (installed ? isLoadingInstalledPlugins : isLoadingAvailablePlugins) || isLoadingPendingPlugins;
|
||||
|
||||
class PluginsOverview extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showPendingModal: false,
|
||||
showExecutePendingModal: false,
|
||||
showUpdateAllModal: false,
|
||||
showCancelModal: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchPlugins();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { installed } = this.props;
|
||||
if (prevProps.installed !== installed) {
|
||||
this.fetchPlugins();
|
||||
}
|
||||
}
|
||||
|
||||
fetchPlugins = () => {
|
||||
const {
|
||||
installed,
|
||||
fetchPluginsByLink,
|
||||
availablePluginsLink,
|
||||
installedPluginsLink,
|
||||
pendingPluginsLink,
|
||||
fetchPendingPlugins
|
||||
} = this.props;
|
||||
fetchPluginsByLink(installed ? installedPluginsLink : availablePluginsLink);
|
||||
if (pendingPluginsLink) {
|
||||
fetchPendingPlugins(pendingPluginsLink);
|
||||
}
|
||||
};
|
||||
|
||||
renderHeader = (actions: React.ReactNode) => {
|
||||
const { installed, t } = this.props;
|
||||
const renderHeader = (actions: React.ReactNode) => {
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
@@ -129,15 +93,14 @@ class PluginsOverview extends React.Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = (actions: React.ReactNode) => {
|
||||
const renderFooter = (actions: React.ReactNode) => {
|
||||
if (actions) {
|
||||
return <PluginBottomActions>{actions}</PluginBottomActions>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
createActions = () => {
|
||||
const { pendingPlugins, collection, t } = this.props;
|
||||
const createActions = () => {
|
||||
const buttons = [];
|
||||
|
||||
if (pendingPlugins && pendingPlugins._links) {
|
||||
@@ -149,11 +112,7 @@ class PluginsOverview extends React.Component<Props, State> {
|
||||
key={"executePending"}
|
||||
icon={"arrow-circle-right"}
|
||||
label={t("plugins.executePending")}
|
||||
action={() =>
|
||||
this.setState({
|
||||
showExecutePendingModal: true
|
||||
})
|
||||
}
|
||||
action={() => setShowExecutePendingModal(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -167,11 +126,7 @@ class PluginsOverview extends React.Component<Props, State> {
|
||||
key={"showPending"}
|
||||
icon={"info"}
|
||||
label={t("plugins.showPending")}
|
||||
action={() =>
|
||||
this.setState({
|
||||
showPendingModal: true
|
||||
})
|
||||
}
|
||||
action={() => setShowPendingModal(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -183,11 +138,7 @@ class PluginsOverview extends React.Component<Props, State> {
|
||||
key={"cancelPending"}
|
||||
icon={"times"}
|
||||
label={t("plugins.cancelPending")}
|
||||
action={() =>
|
||||
this.setState({
|
||||
showCancelModal: true
|
||||
})
|
||||
}
|
||||
action={() => setShowCancelModal(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -200,12 +151,8 @@ class PluginsOverview extends React.Component<Props, State> {
|
||||
reducedMobile={true}
|
||||
key={"updateAll"}
|
||||
icon={"sync-alt"}
|
||||
label={this.computeUpdateAllSize()}
|
||||
action={() =>
|
||||
this.setState({
|
||||
showUpdateAllModal: true
|
||||
})
|
||||
}
|
||||
label={computeUpdateAllSize()}
|
||||
action={() => setShowUpdateAllModal(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -216,133 +163,74 @@ class PluginsOverview extends React.Component<Props, State> {
|
||||
return null;
|
||||
};
|
||||
|
||||
computeUpdateAllSize = () => {
|
||||
const { collection, t } = this.props;
|
||||
const outdatedPlugins = collection._embedded.plugins.filter((p: Plugin) => p._links.update).length;
|
||||
const computeUpdateAllSize = () => {
|
||||
const outdatedPlugins = collection?._embedded.plugins.filter((p: Plugin) => p._links.update).length;
|
||||
return t("plugins.outdatedPlugins", {
|
||||
count: outdatedPlugins
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, collection } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
const renderPluginsList = () => {
|
||||
if (collection?._embedded && collection._embedded.plugins.length > 0) {
|
||||
return <PluginsList plugins={collection._embedded.plugins} openModal={setPluginModalContent} />;
|
||||
}
|
||||
return <Notification type="info">{t("plugins.noPlugins")}</Notification>;
|
||||
};
|
||||
|
||||
if (!collection || loading) {
|
||||
return <Loading />;
|
||||
const renderModals = () => {
|
||||
if (showPendingModal && pendingPlugins) {
|
||||
return <ShowPendingModal onClose={() => setShowPendingModal(false)} pendingPlugins={pendingPlugins} />;
|
||||
}
|
||||
|
||||
const actions = this.createActions();
|
||||
return (
|
||||
<>
|
||||
{this.renderHeader(actions)}
|
||||
<hr className="header-with-actions" />
|
||||
{this.renderPluginsList()}
|
||||
{this.renderFooter(actions)}
|
||||
{this.renderModals()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderModals = () => {
|
||||
const { collection, pendingPlugins } = this.props;
|
||||
const { showPendingModal, showExecutePendingModal, showCancelModal, showUpdateAllModal } = this.state;
|
||||
|
||||
if (showPendingModal) {
|
||||
if (showExecutePendingModal && pendingPlugins) {
|
||||
return (
|
||||
<ShowPendingModal
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
showPendingModal: false
|
||||
})
|
||||
}
|
||||
pendingPlugins={pendingPlugins}
|
||||
/>
|
||||
<ExecutePendingActionModal onClose={() => setShowExecutePendingModal(false)} pendingPlugins={pendingPlugins} />
|
||||
);
|
||||
}
|
||||
|
||||
if (showExecutePendingModal) {
|
||||
return (
|
||||
<ExecutePendingActionModal
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
showExecutePendingModal: false
|
||||
})
|
||||
}
|
||||
pendingPlugins={pendingPlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (showCancelModal) {
|
||||
if (showCancelModal && pendingPlugins) {
|
||||
return (
|
||||
<CancelPendingActionModal
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
showCancelModal: false
|
||||
})
|
||||
}
|
||||
refresh={this.fetchPlugins}
|
||||
onClose={() => setShowCancelModal(false)}
|
||||
pendingPlugins={pendingPlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (showUpdateAllModal) {
|
||||
if (showUpdateAllModal && collection) {
|
||||
return (
|
||||
<UpdateAllActionModal
|
||||
onClose={() =>
|
||||
this.setState({
|
||||
showUpdateAllModal: false
|
||||
})
|
||||
}
|
||||
refresh={this.fetchPlugins}
|
||||
onClose={() => setShowUpdateAllModal(false)}
|
||||
installedPlugins={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (pluginModalContent) {
|
||||
const { action, plugin } = pluginModalContent;
|
||||
return <PluginModal
|
||||
plugin={plugin}
|
||||
pluginAction={action}
|
||||
onClose={() => setPluginModalContent(null)}
|
||||
/>
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderPluginsList() {
|
||||
const { collection, t } = this.props;
|
||||
|
||||
if (collection._embedded && collection._embedded.plugins.length > 0) {
|
||||
return <PluginsList plugins={collection._embedded.plugins} refresh={this.fetchPlugins} />;
|
||||
}
|
||||
return <Notification type="info">{t("plugins.noPlugins")}</Notification>;
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const collection = getPluginCollection(state);
|
||||
const loading = isFetchPluginsPending(state);
|
||||
const error = getFetchPluginsFailure(state);
|
||||
const availablePluginsLink = mustGetAvailablePluginsLink(state);
|
||||
const installedPluginsLink = mustGetInstalledPluginsLink(state);
|
||||
const pendingPluginsLink = getPendingPluginsLink(state);
|
||||
const pendingPlugins = getPendingPlugins(state);
|
||||
if (!collection || loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
loading,
|
||||
error,
|
||||
availablePluginsLink,
|
||||
installedPluginsLink,
|
||||
pendingPluginsLink,
|
||||
pendingPlugins
|
||||
};
|
||||
const actions = createActions();
|
||||
return (
|
||||
<>
|
||||
{renderHeader(actions)}
|
||||
<hr className="header-with-actions" />
|
||||
{renderPluginsList()}
|
||||
{renderFooter(actions)}
|
||||
{renderModals()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchPluginsByLink: (link: string) => {
|
||||
dispatch(fetchPluginsByLink(link));
|
||||
},
|
||||
fetchPendingPlugins: (link: string) => {
|
||||
dispatch(fetchPendingPlugins(link));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(withTranslation("admin"), connect(mapStateToProps, mapDispatchToProps))(PluginsOverview);
|
||||
export default PluginsOverview;
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import reducer, {
|
||||
FETCH_PLUGIN,
|
||||
FETCH_PLUGIN_FAILURE,
|
||||
FETCH_PLUGIN_PENDING,
|
||||
FETCH_PLUGIN_SUCCESS,
|
||||
FETCH_PLUGINS,
|
||||
FETCH_PLUGINS_FAILURE,
|
||||
FETCH_PLUGINS_PENDING,
|
||||
FETCH_PLUGINS_SUCCESS,
|
||||
fetchPluginByLink,
|
||||
fetchPluginByName,
|
||||
fetchPluginsByLink,
|
||||
fetchPluginsSuccess,
|
||||
fetchPluginSuccess,
|
||||
getFetchPluginFailure,
|
||||
getFetchPluginsFailure,
|
||||
getPlugin,
|
||||
getPluginCollection,
|
||||
isFetchPluginPending,
|
||||
isFetchPluginsPending
|
||||
} from "./plugins";
|
||||
import { Plugin, PluginCollection } from "@scm-manager/ui-types";
|
||||
|
||||
const groupManagerPlugin: Plugin = {
|
||||
name: "scm-groupmanager-plugin",
|
||||
bundles: ["/scm/groupmanager-plugin.bundle.js"],
|
||||
type: "Administration",
|
||||
version: "2.0.0-SNAPSHOT",
|
||||
author: "Sebastian Sdorra",
|
||||
description: "Notify a remote webserver whenever a plugin is pushed to.",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scriptPlugin: Plugin = {
|
||||
name: "scm-script-plugin",
|
||||
bundles: ["/scm/script-plugin.bundle.js"],
|
||||
type: "Miscellaneous",
|
||||
version: "2.0.0-SNAPSHOT",
|
||||
author: "Sebastian Sdorra",
|
||||
description: "Script support for scm-manager.",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/v2/ui/plugins/scm-script-plugin"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const branchwpPlugin: Plugin = {
|
||||
name: "scm-branchwp-plugin",
|
||||
bundles: ["/scm/branchwp-plugin.bundle.js"],
|
||||
type: "Miscellaneous",
|
||||
version: "2.0.0-SNAPSHOT",
|
||||
author: "Sebastian Sdorra",
|
||||
description: "This plugin adds branch write protection for plugins.",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/v2/ui/plugins/scm-branchwp-plugin"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const pluginCollectionWithNames: PluginCollection = {
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/v2/ui/plugins"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
plugins: [groupManagerPlugin.name, scriptPlugin.name, branchwpPlugin.name]
|
||||
}
|
||||
};
|
||||
|
||||
const pluginCollection: PluginCollection = {
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/v2/ui/plugins"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
plugins: [groupManagerPlugin, scriptPlugin, branchwpPlugin]
|
||||
}
|
||||
};
|
||||
|
||||
describe("plugins fetch", () => {
|
||||
const URL = "ui/plugins";
|
||||
const PLUGINS_URL = "/api/v2/ui/plugins";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should successfully fetch plugins from link", () => {
|
||||
fetchMock.getOnce(PLUGINS_URL, pluginCollection);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_PLUGINS_PENDING
|
||||
},
|
||||
{
|
||||
type: FETCH_PLUGINS_SUCCESS,
|
||||
payload: pluginCollection
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchPluginsByLink(URL)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_PLUGINS_FAILURE if request fails", () => {
|
||||
fetchMock.getOnce(PLUGINS_URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchPluginsByLink(URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_PLUGINS_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_PLUGINS_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch scm-groupmanager-plugin by name", () => {
|
||||
fetchMock.getOnce(PLUGINS_URL + "/scm-groupmanager-plugin", groupManagerPlugin);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_PLUGIN_PENDING,
|
||||
payload: {
|
||||
name: "scm-groupmanager-plugin"
|
||||
},
|
||||
itemId: "scm-groupmanager-plugin"
|
||||
},
|
||||
{
|
||||
type: FETCH_PLUGIN_SUCCESS,
|
||||
payload: groupManagerPlugin,
|
||||
itemId: "scm-groupmanager-plugin"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchPluginByName(URL, "scm-groupmanager-plugin")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_PLUGIN_FAILURE, if the request for scm-groupmanager-plugin by name fails", () => {
|
||||
fetchMock.getOnce(PLUGINS_URL + "/scm-groupmanager-plugin", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchPluginByName(URL, "scm-groupmanager-plugin")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_PLUGIN_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_PLUGIN_FAILURE);
|
||||
expect(actions[1].payload.name).toBe("scm-groupmanager-plugin");
|
||||
expect(actions[1].payload.error).toBeDefined();
|
||||
expect(actions[1].itemId).toBe("scm-groupmanager-plugin");
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch scm-groupmanager-plugin", () => {
|
||||
fetchMock.getOnce("http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin", groupManagerPlugin);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_PLUGIN_PENDING,
|
||||
payload: {
|
||||
name: "scm-groupmanager-plugin"
|
||||
},
|
||||
itemId: "scm-groupmanager-plugin"
|
||||
},
|
||||
{
|
||||
type: FETCH_PLUGIN_SUCCESS,
|
||||
payload: groupManagerPlugin,
|
||||
itemId: "scm-groupmanager-plugin"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchPluginByLink(groupManagerPlugin)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_PLUGIN_FAILURE, it the request for scm-groupmanager-plugin fails", () => {
|
||||
fetchMock.getOnce("http://localhost:8081/api/v2/ui/plugins/scm-groupmanager-plugin", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchPluginByLink(groupManagerPlugin)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_PLUGIN_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_PLUGIN_FAILURE);
|
||||
expect(actions[1].payload.name).toBe("scm-groupmanager-plugin");
|
||||
expect(actions[1].payload.error).toBeDefined();
|
||||
expect(actions[1].itemId).toBe("scm-groupmanager-plugin");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugins reducer", () => {
|
||||
it("should return empty object, if state and action is undefined", () => {
|
||||
expect(reducer()).toEqual({});
|
||||
});
|
||||
|
||||
it("should return the same state, if the action is undefined", () => {
|
||||
const state = {
|
||||
x: true
|
||||
};
|
||||
expect(reducer(state)).toBe(state);
|
||||
});
|
||||
|
||||
it("should return the same state, if the action is unknown to the reducer", () => {
|
||||
const state = {
|
||||
x: true
|
||||
};
|
||||
expect(
|
||||
reducer(state, {
|
||||
type: "EL_SPECIALE"
|
||||
})
|
||||
).toBe(state);
|
||||
});
|
||||
|
||||
it("should store the plugins by it's type and name on FETCH_PLUGINS_SUCCESS", () => {
|
||||
const newState = reducer({}, fetchPluginsSuccess(pluginCollection));
|
||||
expect(newState.list._embedded.plugins).toEqual([
|
||||
"scm-groupmanager-plugin",
|
||||
"scm-script-plugin",
|
||||
"scm-branchwp-plugin"
|
||||
]);
|
||||
expect(newState.byNames["scm-groupmanager-plugin"]).toBe(groupManagerPlugin);
|
||||
expect(newState.byNames["scm-script-plugin"]).toBe(scriptPlugin);
|
||||
expect(newState.byNames["scm-branchwp-plugin"]).toBe(branchwpPlugin);
|
||||
});
|
||||
|
||||
it("should store the plugin at byNames", () => {
|
||||
const newState = reducer({}, fetchPluginSuccess(groupManagerPlugin));
|
||||
expect(newState.byNames["scm-groupmanager-plugin"]).toBe(groupManagerPlugin);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugins selectors", () => {
|
||||
const error = new Error("something went wrong");
|
||||
|
||||
it("should return the plugins collection", () => {
|
||||
const state = {
|
||||
plugins: {
|
||||
list: pluginCollectionWithNames,
|
||||
byNames: {
|
||||
"scm-groupmanager-plugin": groupManagerPlugin,
|
||||
"scm-script-plugin": scriptPlugin,
|
||||
"scm-branchwp-plugin": branchwpPlugin
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collection = getPluginCollection(state);
|
||||
expect(collection).toEqual(pluginCollection);
|
||||
});
|
||||
|
||||
it("should return true, when fetch plugins is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_PLUGINS]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchPluginsPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch plugins is not pending", () => {
|
||||
expect(isFetchPluginsPending({})).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch plugins did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_PLUGINS]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchPluginsFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch plugins did not fail", () => {
|
||||
expect(getFetchPluginsFailure({})).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return the plugin collection", () => {
|
||||
const state = {
|
||||
plugins: {
|
||||
byNames: {
|
||||
"scm-groupmanager-plugin": groupManagerPlugin
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const plugin = getPlugin(state, "scm-groupmanager-plugin");
|
||||
expect(plugin).toEqual(groupManagerPlugin);
|
||||
});
|
||||
|
||||
it("should return true, when fetch plugin is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_PLUGIN + "/scm-groupmanager-plugin"]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchPluginPending(state, "scm-groupmanager-plugin")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch plugin is not pending", () => {
|
||||
expect(isFetchPluginPending({}, "scm-groupmanager-plugin")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch plugin did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_PLUGIN + "/scm-groupmanager-plugin"]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchPluginFailure(state, "scm-groupmanager-plugin")).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch plugin did not fail", () => {
|
||||
expect(getFetchPluginFailure({}, "scm-groupmanager-plugin")).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -1,277 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import * as types from "../../../modules/types";
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
import { 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}`;
|
||||
|
||||
export const FETCH_PENDING_PLUGINS = "scm/plugins/FETCH_PENDING_PLUGINS";
|
||||
export const FETCH_PENDING_PLUGINS_PENDING = `${FETCH_PENDING_PLUGINS}_${types.PENDING_SUFFIX}`;
|
||||
export const FETCH_PENDING_PLUGINS_SUCCESS = `${FETCH_PENDING_PLUGINS}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_PENDING_PLUGINS_FAILURE = `${FETCH_PENDING_PLUGINS}_${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
|
||||
};
|
||||
}
|
||||
|
||||
// fetch pending plugins
|
||||
export function fetchPendingPlugins(link: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchPendingPluginsPending());
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(PendingPlugins => {
|
||||
dispatch(fetchPendingPluginsSuccess(PendingPlugins));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchPendingPluginsFailure(err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPendingPluginsPending(): Action {
|
||||
return {
|
||||
type: FETCH_PENDING_PLUGINS_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPendingPluginsSuccess(PendingPlugins: {}): Action {
|
||||
return {
|
||||
type: FETCH_PENDING_PLUGINS_SUCCESS,
|
||||
payload: PendingPlugins
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPendingPluginsFailure(err: Error): Action {
|
||||
return {
|
||||
type: FETCH_PENDING_PLUGINS_FAILURE,
|
||||
payload: err
|
||||
};
|
||||
}
|
||||
|
||||
// reducer
|
||||
function normalizeByName(state: object, pluginCollection: PluginCollection) {
|
||||
const names = [];
|
||||
const byNames = {};
|
||||
for (const plugin of pluginCollection._embedded.plugins) {
|
||||
names.push(plugin.name);
|
||||
byNames[plugin.name] = plugin;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
list: {
|
||||
...pluginCollection,
|
||||
_embedded: {
|
||||
plugins: names
|
||||
}
|
||||
},
|
||||
byNames: byNames
|
||||
};
|
||||
}
|
||||
|
||||
const reducerByNames = (state: object, plugin: Plugin) => {
|
||||
return {
|
||||
...state,
|
||||
byNames: {
|
||||
...state.byNames,
|
||||
[plugin.name]: plugin
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: object = {},
|
||||
action: Action = {
|
||||
type: "UNKNOWN"
|
||||
}
|
||||
): object {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case FETCH_PLUGINS_SUCCESS:
|
||||
return normalizeByName(state, action.payload);
|
||||
case FETCH_PLUGIN_SUCCESS:
|
||||
return reducerByNames(state, action.payload);
|
||||
case FETCH_PENDING_PLUGINS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
pending: action.payload
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// selectors
|
||||
export function getPluginCollection(state: object) {
|
||||
if (state.plugins && state.plugins.list && state.plugins.byNames) {
|
||||
const plugins = [];
|
||||
for (const pluginName of state.plugins.list._embedded.plugins) {
|
||||
plugins.push(state.plugins.byNames[pluginName]);
|
||||
}
|
||||
return {
|
||||
...state.plugins.list,
|
||||
_embedded: {
|
||||
plugins
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchPluginsPending(state: object) {
|
||||
return isPending(state, FETCH_PLUGINS);
|
||||
}
|
||||
|
||||
export function getFetchPluginsFailure(state: object) {
|
||||
return getFailure(state, FETCH_PLUGINS);
|
||||
}
|
||||
|
||||
export function getPlugin(state: object, name: string) {
|
||||
if (state.plugins && state.plugins.byNames) {
|
||||
return state.plugins.byNames[name];
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchPluginPending(state: object, name: string) {
|
||||
return isPending(state, FETCH_PLUGIN, name);
|
||||
}
|
||||
|
||||
export function getFetchPluginFailure(state: object, name: string) {
|
||||
return getFailure(state, FETCH_PLUGIN, name);
|
||||
}
|
||||
|
||||
export function getPendingPlugins(state: object) {
|
||||
if (state.plugins && state.plugins.pending) {
|
||||
return state.plugins.pending;
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchPendingPluginsPending(state: object) {
|
||||
return isPending(state, FETCH_PENDING_PLUGINS);
|
||||
}
|
||||
|
||||
export function getFetchPendingPluginsFailure(state: object) {
|
||||
return getFailure(state, FETCH_PENDING_PLUGINS);
|
||||
}
|
||||
Reference in New Issue
Block a user